##// END OF EJS Templates
Merged Git support branch (r1200 to r1226)....
Jean-Philippe Lang -
r1222:3a9b0988c751
parent child
Show More
@@ -0,0 +1,70
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 # Copyright (C) 2007 Patrick Aljord patcito@Ε‹mail.com
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/git_adapter'
19
20 class Repository::Git < Repository
21 attr_protected :root_url
22 validates_presence_of :url
23
24 def scm_adapter
25 Redmine::Scm::Adapters::GitAdapter
26 end
27
28 def self.scm_name
29 'Git'
30 end
31
32 def changesets_for_path(path)
33 Change.find(:all, :include => :changeset,
34 :conditions => ["repository_id = ? AND path = ?", id, path],
35 :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
36 end
37
38 def fetch_changesets
39 scm_info = scm.info
40 if scm_info
41 # latest revision found in database
42 db_revision = latest_changeset ? latest_changeset.revision : nil
43 # latest revision in the repository
44 scm_revision = scm_info.lastrev.scmid
45
46 unless changesets.find_by_scmid(scm_revision)
47
48 revisions = scm.revisions('', db_revision, nil)
49 transaction do
50 revisions.reverse_each do |revision|
51 changeset = Changeset.create(:repository => self,
52 :revision => revision.identifier,
53 :scmid => revision.scmid,
54 :committer => revision.author,
55 :committed_on => revision.time,
56 :comments => revision.message)
57
58 revision.paths.each do |change|
59 Change.create(:changeset => changeset,
60 :action => change[:action],
61 :path => change[:path],
62 :from_path => change[:from_path],
63 :from_revision => change[:from_revision])
64 end
65 end
66 end
67 end
68 end
69 end
70 end
@@ -0,0 +1,9
1 class ChangeChangesetsRevisionToString < ActiveRecord::Migration
2 def self.up
3 change_column :changesets, :revision, :string, :null => false
4 end
5
6 def self.down
7 change_column :changesets, :revision, :integer, :null => false
8 end
9 end
@@ -0,0 +1,9
1 class ChangeChangesFromRevisionToString < ActiveRecord::Migration
2 def self.up
3 change_column :changes, :from_revision, :string
4 end
5
6 def self.down
7 change_column :changes, :from_revision, :integer
8 end
9 end
@@ -0,0 +1,261
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 GitAdapter < AbstractAdapter
24
25 # Git executable name
26 GIT_BIN = "git"
27
28 # Get the revision of a particuliar file
29 def get_rev (rev,path)
30 cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
31 cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if
32 rev=='latest' or rev.nil?
33 rev=[]
34 i=0
35 shellout(cmd) do |io|
36 files=[]
37 changeset = {}
38 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
39
40 io.each_line do |line|
41 if line =~ /^commit ([0-9a-f]{40})$/
42 key = "commit"
43 value = $1
44 if (parsing_descr == 1 || parsing_descr == 2)
45 parsing_descr = 0
46 rev = Revision.new({:identifier => changeset[:commit],
47 :scmid => changeset[:commit],
48 :author => changeset[:author],
49 :time => Time.parse(changeset[:date]),
50 :message => changeset[:description],
51 :paths => files
52 })
53 changeset = {}
54 files = []
55 end
56 changeset[:commit] = $1
57 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
58 key = $1
59 value = $2
60 if key == "Author"
61 changeset[:author] = value
62 elsif key == "Date"
63 changeset[:date] = value
64 end
65 elsif (parsing_descr == 0) && line.chomp.to_s == ""
66 parsing_descr = 1
67 changeset[:description] = ""
68 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
69 parsing_descr = 2
70 fileaction = $1
71 filepath = $2
72 files << {:action => fileaction, :path => filepath}
73 elsif (parsing_descr == 1) && line.chomp.to_s == ""
74 parsing_descr = 2
75 elsif (parsing_descr == 1)
76 changeset[:description] << line
77 end
78 end
79 rev = Revision.new({:identifier => changeset[:commit],
80 :scmid => changeset[:commit],
81 :author => changeset[:author],
82 :time => Time.parse(changeset[:date]),
83 :message => changeset[:description],
84 :paths => files
85 })
86
87 end
88
89 get_rev('latest',path) if rev == []
90
91 return nil if $? && $?.exitstatus != 0
92 return rev
93 end
94
95
96 def info
97 root_url = target('')
98 info = Info.new({:root_url => target(''),
99 :lastrev => revisions(root_url,nil,nil,{:limit => 1}).first
100 })
101 info
102 rescue Errno::ENOENT => e
103 return nil
104 end
105
106 def entries(path=nil, identifier=nil)
107 path ||= ''
108 entries = Entries.new
109 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
110 cmd << shell_quote("HEAD:" + path) if identifier.nil?
111 cmd << shell_quote(identifier + ":" + path) if identifier
112 shellout(cmd) do |io|
113 io.each_line do |line|
114 e = line.chomp.to_s
115 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
116 type = $1
117 sha = $2
118 size = $3
119 name = $4
120 entries << Entry.new({:name => name,
121 :path => (path.empty? ? name : "#{path}/#{name}"),
122 :kind => ((type == "tree") ? 'dir' : 'file'),
123 :size => ((type == "tree") ? nil : size),
124 :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}"))
125
126 }) unless entries.detect{|entry| entry.name == name}
127 end
128 end
129 end
130 return nil if $? && $?.exitstatus != 0
131 entries.sort_by_name
132 end
133
134 def entry(path=nil, identifier=nil)
135 path ||= ''
136 search_path = path.split('/')[0..-2].join('/')
137 entry_name = path.split('/').last
138 e = entries(search_path, identifier)
139 e ? e.detect{|entry| entry.name == entry_name} : nil
140 end
141
142 def revisions(path, identifier_from, identifier_to, options={})
143 revisions = Revisions.new
144 cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
145 cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
146 cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
147 cmd << " #{shell_quote identifier_to} " if identifier_to
148 #cmd << " HEAD " if !identifier_to
149 shellout(cmd) do |io|
150 files=[]
151 changeset = {}
152 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
153 revno = 1
154
155 io.each_line do |line|
156 if line =~ /^commit ([0-9a-f]{40})$/
157 key = "commit"
158 value = $1
159 if (parsing_descr == 1 || parsing_descr == 2)
160 parsing_descr = 0
161 revisions << Revision.new({:identifier => changeset[:commit],
162 :scmid => changeset[:commit],
163 :author => changeset[:author],
164 :time => Time.parse(changeset[:date]),
165 :message => changeset[:description],
166 :paths => files
167 })
168 changeset = {}
169 files = []
170 revno = revno + 1
171 end
172 changeset[:commit] = $1
173 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
174 key = $1
175 value = $2
176 if key == "Author"
177 changeset[:author] = value
178 elsif key == "Date"
179 changeset[:date] = value
180 end
181 elsif (parsing_descr == 0) && line.chomp.to_s == ""
182 parsing_descr = 1
183 changeset[:description] = ""
184 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
185 parsing_descr = 2
186 fileaction = $1
187 filepath = $2
188 files << {:action => fileaction, :path => filepath}
189 elsif (parsing_descr == 1) && line.chomp.to_s == ""
190 parsing_descr = 2
191 elsif (parsing_descr == 1)
192 changeset[:description] << line[4..-1]
193 end
194 end
195
196 revisions << Revision.new({:identifier => changeset[:commit],
197 :scmid => changeset[:commit],
198 :author => changeset[:author],
199 :time => Time.parse(changeset[:date]),
200 :message => changeset[:description],
201 :paths => files
202 }) if changeset[:commit]
203
204 end
205
206 return nil if $? && $?.exitstatus != 0
207 revisions
208 end
209
210 def diff(path, identifier_from, identifier_to=nil, type="inline")
211 path ||= ''
212 if !identifier_to
213 identifier_to = nil
214 end
215
216 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
217 cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
218 cmd << " -- #{shell_quote path}" unless path.empty?
219 diff = []
220 shellout(cmd) do |io|
221 io.each_line do |line|
222 diff << line
223 end
224 end
225 return nil if $? && $?.exitstatus != 0
226 DiffTableList.new diff, type
227 end
228
229 def annotate(path, identifier=nil)
230 identifier = 'HEAD' if identifier.blank?
231 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
232 blame = Annotate.new
233 shellout(cmd) do |io|
234 io.each_line do |line|
235 next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)$/
236 blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
237 end
238 end
239 return nil if $? && $?.exitstatus != 0
240 blame
241 end
242
243 def cat(path, identifier=nil)
244 if identifier.nil?
245 identifier = 'HEAD'
246 end
247 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
248 cat = nil
249 shellout(cmd) do |io|
250 io.binmode
251 cat = io.read
252 end
253 return nil if $? && $?.exitstatus != 0
254 cat
255 end
256 end
257 end
258 end
259
260 end
261
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,94
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'repositories_controller'
20
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
24 class RepositoriesDarcsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
26
27 # No '..' in the repository path
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
29
30 def setup
31 @controller = RepositoriesController.new
32 @request = ActionController::TestRequest.new
33 @response = ActionController::TestResponse.new
34 User.current = nil
35 Repository::Darcs.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36 end
37
38 if File.directory?(REPOSITORY_PATH)
39 def test_show
40 get :show, :id => 3
41 assert_response :success
42 assert_template 'show'
43 assert_not_nil assigns(:entries)
44 assert_not_nil assigns(:changesets)
45 end
46
47 def test_browse_root
48 get :browse, :id => 3
49 assert_response :success
50 assert_template 'browse'
51 assert_not_nil assigns(:entries)
52 assert_equal 3, assigns(:entries).size
53 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
54 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
55 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
56 end
57
58 def test_browse_directory
59 get :browse, :id => 3, :path => ['images']
60 assert_response :success
61 assert_template 'browse'
62 assert_not_nil assigns(:entries)
63 assert_equal 2, assigns(:entries).size
64 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
65 assert_not_nil entry
66 assert_equal 'file', entry.kind
67 assert_equal 'images/edit.png', entry.path
68 end
69
70 def test_changes
71 get :changes, :id => 3, :path => ['images', 'edit.png']
72 assert_response :success
73 assert_template 'changes'
74 assert_tag :tag => 'h2', :content => 'edit.png'
75 end
76
77 def test_diff
78 Project.find(3).repository.fetch_changesets
79 # Full diff of changeset 5
80 get :diff, :id => 3, :rev => 5
81 assert_response :success
82 assert_template 'diff'
83 # Line 22 removed
84 assert_tag :tag => 'th',
85 :content => /22/,
86 :sibling => { :tag => 'td',
87 :attributes => { :class => /diff_out/ },
88 :content => /def remove/ }
89 end
90 else
91 puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
92 def test_fake; assert true end
93 end
94 end
@@ -0,0 +1,123
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 File.dirname(__FILE__) + '/../test_helper'
19 require 'repositories_controller'
20
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
24 class RepositoriesGitControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
26
27 # No '..' in the repository path
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
29 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
30
31 def setup
32 @controller = RepositoriesController.new
33 @request = ActionController::TestRequest.new
34 @response = ActionController::TestResponse.new
35 User.current = nil
36 Repository::Git.create(:project => Project.find(3), :url => REPOSITORY_PATH)
37 end
38
39 if File.directory?(REPOSITORY_PATH)
40 def test_show
41 get :show, :id => 3
42 assert_response :success
43 assert_template 'show'
44 assert_not_nil assigns(:entries)
45 assert_not_nil assigns(:changesets)
46 end
47
48 def test_browse_root
49 get :browse, :id => 3
50 assert_response :success
51 assert_template 'browse'
52 assert_not_nil assigns(:entries)
53 assert_equal 3, assigns(:entries).size
54 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
55 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
56 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
57 end
58
59 def test_browse_directory
60 get :browse, :id => 3, :path => ['images']
61 assert_response :success
62 assert_template 'browse'
63 assert_not_nil assigns(:entries)
64 assert_equal 2, assigns(:entries).size
65 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
66 assert_not_nil entry
67 assert_equal 'file', entry.kind
68 assert_equal 'images/edit.png', entry.path
69 end
70
71 def test_changes
72 get :changes, :id => 3, :path => ['images', 'edit.png']
73 assert_response :success
74 assert_template 'changes'
75 assert_tag :tag => 'h2', :content => 'edit.png'
76 end
77
78 def test_entry_show
79 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
80 assert_response :success
81 assert_template 'entry'
82 # Line 19
83 assert_tag :tag => 'th',
84 :content => /10/,
85 :attributes => { :class => /line-num/ },
86 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
87 end
88
89 def test_entry_download
90 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
91 assert_response :success
92 # File content
93 assert @response.body.include?('WITHOUT ANY WARRANTY')
94 end
95
96 def test_diff
97 # Full diff of changeset 2f9c0091
98 get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
99 assert_response :success
100 assert_template 'diff'
101 # Line 22 removed
102 assert_tag :tag => 'th',
103 :content => /22/,
104 :sibling => { :tag => 'td',
105 :attributes => { :class => /diff_out/ },
106 :content => /def remove/ }
107 end
108
109 def test_annotate
110 get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
111 assert_response :success
112 assert_template 'annotate'
113 # Line 23, changeset 2f9c0091
114 assert_tag :tag => 'th', :content => /23/,
115 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /2f9c0091/ } },
116 :sibling => { :tag => 'td', :content => /jsmith/ },
117 :sibling => { :tag => 'td', :content => /watcher =/ }
118 end
119 else
120 puts "Git test repository NOT FOUND. Skipping functional tests !!!"
121 def test_fake; assert true end
122 end
123 end
@@ -0,0 +1,55
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class RepositoryDarcsTest < Test::Unit::TestCase
21 fixtures :projects
22
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
25
26 def setup
27 @project = Project.find(1)
28 assert @repository = Repository::Darcs.create(:project => @project, :url => REPOSITORY_PATH)
29 end
30
31 if File.directory?(REPOSITORY_PATH)
32 def test_fetch_changesets_from_scratch
33 @repository.fetch_changesets
34 @repository.reload
35
36 assert_equal 6, @repository.changesets.count
37 assert_equal 13, @repository.changes.count
38 assert_equal "Initial commit.", @repository.changesets.find_by_revision(1).comments
39 end
40
41 def test_fetch_changesets_incremental
42 @repository.fetch_changesets
43 # Remove changesets with revision > 3
44 @repository.changesets.find(:all, :conditions => 'revision > 3').each(&:destroy)
45 @repository.reload
46 assert_equal 3, @repository.changesets.count
47
48 @repository.fetch_changesets
49 assert_equal 6, @repository.changesets.count
50 end
51 else
52 puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
53 def test_fake; assert true end
54 end
55 end
@@ -0,0 +1,56
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 File.dirname(__FILE__) + '/../test_helper'
19
20 class RepositoryGitTest < Test::Unit::TestCase
21 fixtures :projects
22
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
25 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
26
27 def setup
28 @project = Project.find(1)
29 assert @repository = Repository::Git.create(:project => @project, :url => REPOSITORY_PATH)
30 end
31
32 if File.directory?(REPOSITORY_PATH)
33 def test_fetch_changesets_from_scratch
34 @repository.fetch_changesets
35 @repository.reload
36
37 assert_equal 6, @repository.changesets.count
38 assert_equal 11, @repository.changes.count
39 assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find(:first, :order => 'id ASC').comments
40 end
41
42 def test_fetch_changesets_incremental
43 @repository.fetch_changesets
44 # Remove the 3 latest changesets
45 @repository.changesets.find(:all, :order => 'id DESC', :limit => 3).each(&:destroy)
46 @repository.reload
47 assert_equal 3, @repository.changesets.count
48
49 @repository.fetch_changesets
50 assert_equal 6, @repository.changesets.count
51 end
52 else
53 puts "Git test repository NOT FOUND. Skipping unit tests !!!"
54 def test_fake; assert true end
55 end
56 end
@@ -1,298 +1,298
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception
23 23 end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 layout 'base'
27 27 menu_item :repository
28 28 before_filter :find_repository, :except => :edit
29 29 before_filter :find_project, :only => :edit
30 30 before_filter :authorize
31 31 accept_key_auth :revisions
32 32
33 33 def edit
34 34 @repository = @project.repository
35 35 if !@repository
36 36 @repository = Repository.factory(params[:repository_scm])
37 37 @repository.project = @project
38 38 end
39 39 if request.post?
40 40 @repository.attributes = params[:repository]
41 41 @repository.save
42 42 end
43 43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
44 44 end
45 45
46 46 def destroy
47 47 @repository.destroy
48 48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
49 49 end
50 50
51 51 def show
52 52 # check if new revisions have been committed in the repository
53 53 @repository.fetch_changesets if Setting.autofetch_changesets?
54 54 # get entries for the browse frame
55 55 @entries = @repository.entries('')
56 56 # latest changesets
57 57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
58 58 show_error_not_found unless @entries || @changesets.any?
59 59 rescue Redmine::Scm::Adapters::CommandFailed => e
60 60 show_error_command_failed(e.message)
61 61 end
62 62
63 63 def browse
64 64 @entries = @repository.entries(@path, @rev)
65 65 if request.xhr?
66 66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
67 67 else
68 68 show_error_not_found unless @entries
69 69 end
70 70 rescue Redmine::Scm::Adapters::CommandFailed => e
71 71 show_error_command_failed(e.message)
72 72 end
73 73
74 74 def changes
75 75 @entry = @repository.scm.entry(@path, @rev)
76 76 show_error_not_found and return unless @entry
77 77 @changesets = @repository.changesets_for_path(@path)
78 78 rescue Redmine::Scm::Adapters::CommandFailed => e
79 79 show_error_command_failed(e.message)
80 80 end
81 81
82 82 def revisions
83 83 @changeset_count = @repository.changesets.count
84 84 @changeset_pages = Paginator.new self, @changeset_count,
85 85 per_page_option,
86 86 params['page']
87 87 @changesets = @repository.changesets.find(:all,
88 88 :limit => @changeset_pages.items_per_page,
89 89 :offset => @changeset_pages.current.offset)
90 90
91 91 respond_to do |format|
92 92 format.html { render :layout => false if request.xhr? }
93 93 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
94 94 end
95 95 end
96 96
97 97 def entry
98 98 @content = @repository.scm.cat(@path, @rev)
99 99 show_error_not_found and return unless @content
100 100 if 'raw' == params[:format]
101 101 send_data @content, :filename => @path.split('/').last
102 102 else
103 103 # Prevent empty lines when displaying a file with Windows style eol
104 104 @content.gsub!("\r\n", "\n")
105 105 end
106 106 rescue Redmine::Scm::Adapters::CommandFailed => e
107 107 show_error_command_failed(e.message)
108 108 end
109 109
110 110 def annotate
111 111 @annotate = @repository.scm.annotate(@path, @rev)
112 112 show_error_not_found and return if @annotate.nil? || @annotate.empty?
113 113 rescue Redmine::Scm::Adapters::CommandFailed => e
114 114 show_error_command_failed(e.message)
115 115 end
116 116
117 117 def revision
118 118 @changeset = @repository.changesets.find_by_revision(@rev)
119 119 raise ChangesetNotFound unless @changeset
120 120 @changes_count = @changeset.changes.size
121 121 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
122 122 @changes = @changeset.changes.find(:all,
123 123 :limit => @changes_pages.items_per_page,
124 124 :offset => @changes_pages.current.offset)
125 125
126 126 respond_to do |format|
127 127 format.html
128 128 format.js {render :layout => false}
129 129 end
130 130 rescue ChangesetNotFound
131 131 show_error_not_found
132 132 rescue Redmine::Scm::Adapters::CommandFailed => e
133 133 show_error_command_failed(e.message)
134 134 end
135 135
136 136 def diff
137 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
137 @rev_to = params[:rev_to]
138 138 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
139 139 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
140 140
141 141 # Save diff type as user preference
142 142 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
143 143 User.current.pref[:diff_type] = @diff_type
144 144 User.current.preference.save
145 145 end
146 146
147 147 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
148 148 unless read_fragment(@cache_key)
149 149 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
150 150 show_error_not_found unless @diff
151 151 end
152 152 rescue Redmine::Scm::Adapters::CommandFailed => e
153 153 show_error_command_failed(e.message)
154 154 end
155 155
156 156 def stats
157 157 end
158 158
159 159 def graph
160 160 data = nil
161 161 case params[:graph]
162 162 when "commits_per_month"
163 163 data = graph_commits_per_month(@repository)
164 164 when "commits_per_author"
165 165 data = graph_commits_per_author(@repository)
166 166 end
167 167 if data
168 168 headers["Content-Type"] = "image/svg+xml"
169 169 send_data(data, :type => "image/svg+xml", :disposition => "inline")
170 170 else
171 171 render_404
172 172 end
173 173 end
174 174
175 175 private
176 176 def find_project
177 177 @project = Project.find(params[:id])
178 178 rescue ActiveRecord::RecordNotFound
179 179 render_404
180 180 end
181 181
182 182 def find_repository
183 183 @project = Project.find(params[:id])
184 184 @repository = @project.repository
185 185 render_404 and return false unless @repository
186 186 @path = params[:path].join('/') unless params[:path].nil?
187 187 @path ||= ''
188 @rev = params[:rev].to_i if params[:rev]
188 @rev = params[:rev]
189 189 rescue ActiveRecord::RecordNotFound
190 190 render_404
191 191 end
192 192
193 193 def show_error_not_found
194 194 render_error l(:error_scm_not_found)
195 195 end
196 196
197 197 def show_error_command_failed(msg)
198 198 render_error l(:error_scm_command_failed, msg)
199 199 end
200 200
201 201 def graph_commits_per_month(repository)
202 202 @date_to = Date.today
203 203 @date_from = @date_to << 11
204 204 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
205 205 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
206 206 commits_by_month = [0] * 12
207 207 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
208 208
209 209 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
210 210 changes_by_month = [0] * 12
211 211 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
212 212
213 213 fields = []
214 214 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
215 215 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
216 216
217 217 graph = SVG::Graph::Bar.new(
218 218 :height => 300,
219 219 :width => 500,
220 220 :fields => fields.reverse,
221 221 :stack => :side,
222 222 :scale_integers => true,
223 223 :step_x_labels => 2,
224 224 :show_data_values => false,
225 225 :graph_title => l(:label_commits_per_month),
226 226 :show_graph_title => true
227 227 )
228 228
229 229 graph.add_data(
230 230 :data => commits_by_month[0..11].reverse,
231 231 :title => l(:label_revision_plural)
232 232 )
233 233
234 234 graph.add_data(
235 235 :data => changes_by_month[0..11].reverse,
236 236 :title => l(:label_change_plural)
237 237 )
238 238
239 239 graph.burn
240 240 end
241 241
242 242 def graph_commits_per_author(repository)
243 243 commits_by_author = repository.changesets.count(:all, :group => :committer)
244 244 commits_by_author.sort! {|x, y| x.last <=> y.last}
245 245
246 246 changes_by_author = repository.changes.count(:all, :group => :committer)
247 247 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
248 248
249 249 fields = commits_by_author.collect {|r| r.first}
250 250 commits_data = commits_by_author.collect {|r| r.last}
251 251 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
252 252
253 253 fields = fields + [""]*(10 - fields.length) if fields.length<10
254 254 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
255 255 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
256 256
257 257 graph = SVG::Graph::BarHorizontal.new(
258 258 :height => 300,
259 259 :width => 500,
260 260 :fields => fields,
261 261 :stack => :side,
262 262 :scale_integers => true,
263 263 :show_data_values => false,
264 264 :rotate_y_labels => false,
265 265 :graph_title => l(:label_commits_per_author),
266 266 :show_graph_title => true
267 267 )
268 268
269 269 graph.add_data(
270 270 :data => commits_data,
271 271 :title => l(:label_revision_plural)
272 272 )
273 273
274 274 graph.add_data(
275 275 :data => changes_data,
276 276 :title => l(:label_change_plural)
277 277 )
278 278
279 279 graph.burn
280 280 end
281 281
282 282 end
283 283
284 284 class Date
285 285 def months_ago(date = Date.today)
286 286 (date.year - self.year)*12 + (date.month - self.month)
287 287 end
288 288
289 289 def weeks_ago(date = Date.today)
290 290 (date.year - self.year)*52 + (date.cweek - self.cweek)
291 291 end
292 292 end
293 293
294 294 class String
295 295 def with_leading_slash
296 296 starts_with?('/') ? self : "/#{self}"
297 297 end
298 298 end
@@ -1,466 +1,471
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module ApplicationHelper
19 19 include Redmine::WikiFormatting::Macros::Definitions
20 20
21 21 def current_role
22 22 @current_role ||= User.current.role_for_project(@project)
23 23 end
24 24
25 25 # Return true if user is authorized for controller/action, otherwise false
26 26 def authorize_for(controller, action)
27 27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
28 28 end
29 29
30 30 # Display a link if user is authorized
31 31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
32 32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
33 33 end
34 34
35 35 # Display a link to user's account page
36 36 def link_to_user(user)
37 37 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
38 38 end
39 39
40 40 def link_to_issue(issue)
41 41 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
42 42 end
43 43
44 44 def toggle_link(name, id, options={})
45 45 onclick = "Element.toggle('#{id}'); "
46 46 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
47 47 onclick << "return false;"
48 48 link_to(name, "#", :onclick => onclick)
49 49 end
50 50
51 51 def show_and_goto_link(name, id, options={})
52 52 onclick = "Element.show('#{id}'); "
53 53 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
54 54 onclick << "Element.scrollTo('#{id}'); "
55 55 onclick << "return false;"
56 56 link_to(name, "#", options.merge(:onclick => onclick))
57 57 end
58 58
59 59 def image_to_function(name, function, html_options = {})
60 60 html_options.symbolize_keys!
61 61 tag(:input, html_options.merge({
62 62 :type => "image", :src => image_path(name),
63 63 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
64 64 }))
65 65 end
66 66
67 67 def prompt_to_remote(name, text, param, url, html_options = {})
68 68 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
69 69 link_to name, {}, html_options
70 70 end
71 71
72 72 def format_date(date)
73 73 return nil unless date
74 74 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
75 75 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
76 76 date.strftime(@date_format)
77 77 end
78 78
79 79 def format_time(time, include_date = true)
80 80 return nil unless time
81 81 time = time.to_time if time.is_a?(String)
82 82 zone = User.current.time_zone
83 83 if time.utc?
84 84 local = zone ? zone.adjust(time) : time.getlocal
85 85 else
86 86 local = zone ? zone.adjust(time.getutc) : time
87 87 end
88 88 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
89 89 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
90 90 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
91 91 end
92 92
93 93 def html_hours(text)
94 94 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
95 95 end
96 96
97 97 def authoring(created, author)
98 98 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
99 99 l(:label_added_time_by, author || 'Anonymous', time_tag)
100 100 end
101 101
102 102 def l_or_humanize(s)
103 103 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
104 104 end
105 105
106 106 def day_name(day)
107 107 l(:general_day_names).split(',')[day-1]
108 108 end
109 109
110 110 def month_name(month)
111 111 l(:actionview_datehelper_select_month_names).split(',')[month-1]
112 112 end
113 113
114 114 def pagination_links_full(paginator, count=nil, options={})
115 115 page_param = options.delete(:page_param) || :page
116 116 url_param = params.dup
117 117 # don't reuse params if filters are present
118 118 url_param.clear if url_param.has_key?(:set_filter)
119 119
120 120 html = ''
121 121 html << link_to_remote(('&#171; ' + l(:label_previous)),
122 122 {:update => 'content',
123 123 :url => url_param.merge(page_param => paginator.current.previous),
124 124 :complete => 'window.scrollTo(0,0)'},
125 125 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
126 126
127 127 html << (pagination_links_each(paginator, options) do |n|
128 128 link_to_remote(n.to_s,
129 129 {:url => {:params => url_param.merge(page_param => n)},
130 130 :update => 'content',
131 131 :complete => 'window.scrollTo(0,0)'},
132 132 {:href => url_for(:params => url_param.merge(page_param => n))})
133 133 end || '')
134 134
135 135 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
136 136 {:update => 'content',
137 137 :url => url_param.merge(page_param => paginator.current.next),
138 138 :complete => 'window.scrollTo(0,0)'},
139 139 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
140 140
141 141 unless count.nil?
142 142 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
143 143 end
144 144
145 145 html
146 146 end
147 147
148 148 def per_page_links(selected=nil)
149 149 url_param = params.dup
150 150 url_param.clear if url_param.has_key?(:set_filter)
151 151
152 152 links = Setting.per_page_options_array.collect do |n|
153 153 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
154 154 {:href => url_for(url_param.merge(:per_page => n))})
155 155 end
156 156 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
157 157 end
158 158
159 159 def html_title(*args)
160 160 if args.empty?
161 161 title = []
162 162 title << @project.name if @project
163 163 title += @html_title if @html_title
164 164 title << Setting.app_title
165 165 title.compact.join(' - ')
166 166 else
167 167 @html_title ||= []
168 168 @html_title += args
169 169 end
170 170 end
171 171
172 172 def accesskey(s)
173 173 Redmine::AccessKeys.key_for s
174 174 end
175 175
176 176 # Formats text according to system settings.
177 177 # 2 ways to call this method:
178 178 # * with a String: textilizable(text, options)
179 179 # * with an object and one of its attribute: textilizable(issue, :description, options)
180 180 def textilizable(*args)
181 181 options = args.last.is_a?(Hash) ? args.pop : {}
182 182 case args.size
183 183 when 1
184 184 obj = nil
185 185 text = args.shift
186 186 when 2
187 187 obj = args.shift
188 188 text = obj.send(args.shift).to_s
189 189 else
190 190 raise ArgumentError, 'invalid arguments to textilizable'
191 191 end
192 192 return '' if text.blank?
193 193
194 194 only_path = options.delete(:only_path) == false ? false : true
195 195
196 196 # when using an image link, try to use an attachment, if possible
197 197 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
198 198
199 199 if attachments
200 200 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
201 201 style = $1
202 202 filename = $6
203 203 rf = Regexp.new(filename, Regexp::IGNORECASE)
204 204 # search for the picture in attachments
205 205 if found = attachments.detect { |att| att.filename =~ rf }
206 206 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id
207 207 "!#{style}#{image_url}!"
208 208 else
209 209 "!#{style}#{filename}!"
210 210 end
211 211 end
212 212 end
213 213
214 214 text = (Setting.text_formatting == 'textile') ?
215 215 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
216 216 simple_format(auto_link(h(text)))
217 217
218 218 # different methods for formatting wiki links
219 219 case options[:wiki_links]
220 220 when :local
221 221 # used for local links to html files
222 222 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
223 223 when :anchor
224 224 # used for single-file wiki export
225 225 format_wiki_link = Proc.new {|project, title| "##{title}" }
226 226 else
227 227 format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
228 228 end
229 229
230 230 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
231 231
232 232 # Wiki links
233 233 #
234 234 # Examples:
235 235 # [[mypage]]
236 236 # [[mypage|mytext]]
237 237 # wiki links can refer other project wikis, using project name or identifier:
238 238 # [[project:]] -> wiki starting page
239 239 # [[project:|mytext]]
240 240 # [[project:mypage]]
241 241 # [[project:mypage|mytext]]
242 242 text = text.gsub(/(!)?(\[\[([^\]\|]+)(\|([^\]\|]+))?\]\])/) do |m|
243 243 link_project = project
244 244 esc, all, page, title = $1, $2, $3, $5
245 245 if esc.nil?
246 246 if page =~ /^([^\:]+)\:(.*)$/
247 247 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
248 248 page = $2
249 249 title ||= $1 if page.blank?
250 250 end
251 251
252 252 if link_project && link_project.wiki
253 253 # check if page exists
254 254 wiki_page = link_project.wiki.find_page(page)
255 255 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
256 256 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
257 257 else
258 258 # project or wiki doesn't exist
259 259 title || page
260 260 end
261 261 else
262 262 all
263 263 end
264 264 end
265 265
266 266 # Redmine links
267 267 #
268 268 # Examples:
269 269 # Issues:
270 270 # #52 -> Link to issue #52
271 271 # Changesets:
272 272 # r52 -> Link to revision 52
273 # commit:a85130f -> Link to scmid starting with a85130f
273 274 # Documents:
274 275 # document#17 -> Link to document with id 17
275 276 # document:Greetings -> Link to the document with title "Greetings"
276 277 # document:"Some document" -> Link to the document with title "Some document"
277 278 # Versions:
278 279 # version#3 -> Link to version with id 3
279 280 # version:1.0.0 -> Link to version named "1.0.0"
280 281 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
281 282 # Attachments:
282 283 # attachment:file.zip -> Link to the attachment of the current object named file.zip
283 text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
284 text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
284 285 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
285 286 link = nil
286 287 if esc.nil?
287 288 if prefix.nil? && sep == 'r'
288 289 if project && (changeset = project.changesets.find_by_revision(oid))
289 290 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid},
290 291 :class => 'changeset',
291 292 :title => truncate(changeset.comments, 100))
292 293 end
293 294 elsif sep == '#'
294 295 oid = oid.to_i
295 296 case prefix
296 297 when nil
297 298 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
298 299 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
299 300 :class => (issue.closed? ? 'issue closed' : 'issue'),
300 301 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
301 302 link = content_tag('del', link) if issue.closed?
302 303 end
303 304 when 'document'
304 305 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
305 306 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
306 307 :class => 'document'
307 308 end
308 309 when 'version'
309 310 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
310 311 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
311 312 :class => 'version'
312 313 end
313 314 end
314 315 elsif sep == ':'
315 316 # removes the double quotes if any
316 317 name = oid.gsub(%r{^"(.*)"$}, "\\1")
317 318 case prefix
318 319 when 'document'
319 320 if project && document = project.documents.find_by_title(name)
320 321 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
321 322 :class => 'document'
322 323 end
323 324 when 'version'
324 325 if project && version = project.versions.find_by_name(name)
325 326 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
326 327 :class => 'version'
327 328 end
329 when 'commit'
330 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
331 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => changeset.revision}, :class => 'changeset', :title => truncate(changeset.comments, 100)
332 end
328 333 when 'attachment'
329 334 if attachments && attachment = attachments.detect {|a| a.filename == name }
330 335 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
331 336 :class => 'attachment'
332 337 end
333 338 end
334 339 end
335 340 end
336 341 leading + (link || "#{prefix}#{sep}#{oid}")
337 342 end
338 343
339 344 text
340 345 end
341 346
342 347 # Same as Rails' simple_format helper without using paragraphs
343 348 def simple_format_without_paragraph(text)
344 349 text.to_s.
345 350 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
346 351 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
347 352 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
348 353 end
349 354
350 355 def error_messages_for(object_name, options = {})
351 356 options = options.symbolize_keys
352 357 object = instance_variable_get("@#{object_name}")
353 358 if object && !object.errors.empty?
354 359 # build full_messages here with controller current language
355 360 full_messages = []
356 361 object.errors.each do |attr, msg|
357 362 next if msg.nil?
358 363 msg = msg.first if msg.is_a? Array
359 364 if attr == "base"
360 365 full_messages << l(msg)
361 366 else
362 367 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
363 368 end
364 369 end
365 370 # retrieve custom values error messages
366 371 if object.errors[:custom_values]
367 372 object.custom_values.each do |v|
368 373 v.errors.each do |attr, msg|
369 374 next if msg.nil?
370 375 msg = msg.first if msg.is_a? Array
371 376 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
372 377 end
373 378 end
374 379 end
375 380 content_tag("div",
376 381 content_tag(
377 382 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
378 383 ) +
379 384 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
380 385 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
381 386 )
382 387 else
383 388 ""
384 389 end
385 390 end
386 391
387 392 def lang_options_for_select(blank=true)
388 393 (blank ? [["(auto)", ""]] : []) +
389 394 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
390 395 end
391 396
392 397 def label_tag_for(name, option_tags = nil, options = {})
393 398 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
394 399 content_tag("label", label_text)
395 400 end
396 401
397 402 def labelled_tabular_form_for(name, object, options, &proc)
398 403 options[:html] ||= {}
399 404 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
400 405 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
401 406 end
402 407
403 408 def check_all_links(form_name)
404 409 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
405 410 " | " +
406 411 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
407 412 end
408 413
409 414 def progress_bar(pcts, options={})
410 415 pcts = [pcts, pcts] unless pcts.is_a?(Array)
411 416 pcts[1] = pcts[1] - pcts[0]
412 417 pcts << (100 - pcts[1] - pcts[0])
413 418 width = options[:width] || '100px;'
414 419 legend = options[:legend] || ''
415 420 content_tag('table',
416 421 content_tag('tr',
417 422 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
418 423 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
419 424 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
420 425 ), :class => 'progress', :style => "width: #{width};") +
421 426 content_tag('p', legend, :class => 'pourcent')
422 427 end
423 428
424 429 def context_menu_link(name, url, options={})
425 430 options[:class] ||= ''
426 431 if options.delete(:selected)
427 432 options[:class] << ' icon-checked disabled'
428 433 options[:disabled] = true
429 434 end
430 435 if options.delete(:disabled)
431 436 options.delete(:method)
432 437 options.delete(:confirm)
433 438 options.delete(:onclick)
434 439 options[:class] << ' disabled'
435 440 url = '#'
436 441 end
437 442 link_to name, url, options
438 443 end
439 444
440 445 def calendar_for(field_id)
441 446 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
442 447 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
443 448 end
444 449
445 450 def wikitoolbar_for(field_id)
446 451 return '' unless Setting.text_formatting == 'textile'
447 452
448 453 help_link = l(:setting_text_formatting) + ': ' +
449 454 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
450 455 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
451 456
452 457 javascript_include_tag('jstoolbar/jstoolbar') +
453 458 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
454 459 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
455 460 end
456 461
457 462 def content_for(name, content = nil, &block)
458 463 @has_content ||= {}
459 464 @has_content[name] = true
460 465 super(name, content, &block)
461 466 end
462 467
463 468 def has_content?(name)
464 469 (@has_content && @has_content[name]) || false
465 470 end
466 471 end
@@ -1,87 +1,95
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'iconv'
21 21
22 22 module RepositoriesHelper
23 23 def syntax_highlight(name, content)
24 24 type = CodeRay::FileType[name]
25 25 type ? CodeRay.scan(content, type).html : h(content)
26 26 end
27 27
28 def format_revision(txt)
29 txt.to_s[0,8]
30 end
31
28 32 def to_utf8(str)
29 33 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
30 34 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
31 35 @encodings.each do |encoding|
32 36 begin
33 37 return Iconv.conv('UTF-8', encoding, str)
34 38 rescue Iconv::Failure
35 39 # do nothing here and try the next encoding
36 40 end
37 41 end
38 42 str
39 43 end
40 44
41 45 def repository_field_tags(form, repository)
42 46 method = repository.class.name.demodulize.underscore + "_field_tags"
43 47 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
44 48 end
45 49
46 50 def scm_select_tag(repository)
47 51 container = [[]]
48 52 REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
49 53 select_tag('repository_scm',
50 54 options_for_select(container, repository.class.name.demodulize),
51 55 :disabled => (repository && !repository.new_record?),
52 56 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
53 57 )
54 58 end
55 59
56 60 def with_leading_slash(path)
57 61 path ||= ''
58 62 path.starts_with?('/') ? path : "/#{path}"
59 63 end
60 64
61 65 def subversion_field_tags(form, repository)
62 66 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
63 67 '<br />(http://, https://, svn://, file:///)') +
64 68 content_tag('p', form.text_field(:login, :size => 30)) +
65 69 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
66 70 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
67 71 :onfocus => "this.value=''; this.name='repository[password]';",
68 72 :onchange => "this.name='repository[password]';"))
69 73 end
70 74
71 75 def darcs_field_tags(form, repository)
72 76 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
73 77 end
74 78
75 79 def mercurial_field_tags(form, repository)
76 80 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
77 81 end
78 82
83 def git_field_tags(form, repository)
84 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
85 end
86
79 87 def cvs_field_tags(form, repository)
80 88 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
81 89 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
82 90 end
83 91
84 92 def bazaar_field_tags(form, repository)
85 93 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
86 94 end
87 95 end
@@ -1,124 +1,127
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 has_many :changes, :dependent => :delete_all
21 21 has_and_belongs_to_many :issues
22 22
23 23 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
24 24 :description => :comments,
25 25 :datetime => :committed_on,
26 26 :author => :committer,
27 27 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
28 28
29 29 acts_as_searchable :columns => 'comments',
30 30 :include => :repository,
31 31 :project_key => "#{Repository.table_name}.project_id",
32 32 :date_column => 'committed_on'
33 33
34 34 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
35 validates_numericality_of :revision, :only_integer => true
36 35 validates_uniqueness_of :revision, :scope => :repository_id
37 36 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
38 37
39 38 def comments=(comment)
40 39 write_attribute(:comments, comment.strip)
41 40 end
42 41
43 42 def committed_on=(date)
44 43 self.commit_date = date
45 44 super
46 45 end
47 46
48 47 def project
49 48 repository.project
50 49 end
51 50
52 51 def after_create
53 52 scan_comment_for_issue_ids
54 53 end
55 54 require 'pp'
56 55
57 56 def scan_comment_for_issue_ids
58 57 return if comments.blank?
59 58 # keywords used to reference issues
60 59 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
61 60 # keywords used to fix issues
62 61 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
63 62 # status and optional done ratio applied
64 63 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
65 64 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
66 65
67 66 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
68 67 return if kw_regexp.blank?
69 68
70 69 referenced_issues = []
71 70
72 71 if ref_keywords.delete('*')
73 72 # find any issue ID in the comments
74 73 target_issue_ids = []
75 74 comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
76 75 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
77 76 end
78 77
79 78 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
80 79 action = match[0]
81 80 target_issue_ids = match[1].scan(/\d+/)
82 81 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
83 82 if fix_status && fix_keywords.include?(action.downcase)
84 83 # update status of issues
85 84 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
86 85 target_issues.each do |issue|
87 86 # the issue may have been updated by the closure of another one (eg. duplicate)
88 87 issue.reload
89 88 # don't change the status is the issue is closed
90 89 next if issue.status.is_closed?
91 90 user = committer_user || User.anonymous
92 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, "r#{self.revision}"))
91 csettext = "r#{self.revision}"
92 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
93 csettext = "commit:\"#{self.scmid}\""
94 end
95 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
93 96 issue.status = fix_status
94 97 issue.done_ratio = done_ratio if done_ratio
95 98 issue.save
96 99 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
97 100 end
98 101 end
99 102 referenced_issues += target_issues
100 103 end
101 104
102 105 self.issues = referenced_issues.uniq
103 106 end
104 107
105 108 # Returns the Redmine User corresponding to the committer
106 109 def committer_user
107 110 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
108 111 username, email = $1.strip, $3
109 112 u = User.find_by_login(username)
110 113 u ||= User.find_by_mail(email) unless email.blank?
111 114 u
112 115 end
113 116 end
114 117
115 118 # Returns the previous changeset
116 119 def previous
117 @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC')
120 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
118 121 end
119 122
120 123 # Returns the next changeset
121 124 def next
122 @next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC')
125 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
123 126 end
124 127 end
@@ -1,91 +1,91
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Repository < ActiveRecord::Base
19 19 belongs_to :project
20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 21 has_many :changes, :through => :changesets
22 22
23 23 def scm
24 24 @scm ||= self.scm_adapter.new url, root_url, login, password
25 25 update_attribute(:root_url, @scm.root_url) if root_url.blank?
26 26 @scm
27 27 end
28 28
29 29 def scm_name
30 30 self.class.scm_name
31 31 end
32 32
33 33 def supports_cat?
34 34 scm.supports_cat?
35 35 end
36 36
37 37 def supports_annotate?
38 38 scm.supports_annotate?
39 39 end
40 40
41 41 def entries(path=nil, identifier=nil)
42 42 scm.entries(path, identifier)
43 43 end
44 44
45 45 def diff(path, rev, rev_to, type)
46 46 scm.diff(path, rev, rev_to, type)
47 47 end
48 48
49 49 # Default behaviour: we search in cached changesets
50 50 def changesets_for_path(path)
51 51 path = "/#{path}" unless path.starts_with?('/')
52 52 Change.find(:all, :include => :changeset,
53 53 :conditions => ["repository_id = ? AND path = ?", id, path],
54 :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
54 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
55 55 end
56 56
57 57 def latest_changeset
58 58 @latest_changeset ||= changesets.find(:first)
59 59 end
60 60
61 61 def scan_changesets_for_issue_ids
62 62 self.changesets.each(&:scan_comment_for_issue_ids)
63 63 end
64 64
65 65 # fetch new changesets for all repositories
66 66 # can be called periodically by an external script
67 67 # eg. ruby script/runner "Repository.fetch_changesets"
68 68 def self.fetch_changesets
69 69 find(:all).each(&:fetch_changesets)
70 70 end
71 71
72 72 # scan changeset comments to find related and fixed issues for all repositories
73 73 def self.scan_changesets_for_issue_ids
74 74 find(:all).each(&:scan_changesets_for_issue_ids)
75 75 end
76 76
77 77 def self.scm_name
78 78 'Abstract'
79 79 end
80 80
81 81 def self.available_scm
82 82 subclasses.collect {|klass| [klass.scm_name, klass.name]}
83 83 end
84 84
85 85 def self.factory(klass_name, *args)
86 86 klass = "Repository::#{klass_name}".constantize
87 87 klass.new(*args)
88 88 rescue
89 89 nil
90 90 end
91 91 end
@@ -1,86 +1,86
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/bazaar_adapter'
19 19
20 20 class Repository::Bazaar < Repository
21 21 attr_protected :root_url
22 22 validates_presence_of :url
23 23
24 24 def scm_adapter
25 25 Redmine::Scm::Adapters::BazaarAdapter
26 26 end
27 27
28 28 def self.scm_name
29 29 'Bazaar'
30 30 end
31 31
32 32 def entries(path=nil, identifier=nil)
33 33 entries = scm.entries(path, identifier)
34 34 if entries
35 35 entries.each do |e|
36 36 next if e.lastrev.revision.blank?
37 37 c = Change.find(:first,
38 38 :include => :changeset,
39 39 :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
40 40 :order => "#{Changeset.table_name}.revision DESC")
41 41 if c
42 42 e.lastrev.identifier = c.changeset.revision
43 43 e.lastrev.name = c.changeset.revision
44 44 e.lastrev.author = c.changeset.committer
45 45 end
46 46 end
47 47 end
48 48 end
49 49
50 50 def fetch_changesets
51 51 scm_info = scm.info
52 52 if scm_info
53 53 # latest revision found in database
54 db_revision = latest_changeset ? latest_changeset.revision : 0
54 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
55 55 # latest revision in the repository
56 56 scm_revision = scm_info.lastrev.identifier.to_i
57 57 if db_revision < scm_revision
58 58 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
59 59 identifier_from = db_revision + 1
60 60 while (identifier_from <= scm_revision)
61 61 # loads changesets by batches of 200
62 62 identifier_to = [identifier_from + 199, scm_revision].min
63 63 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
64 64 transaction do
65 65 revisions.reverse_each do |revision|
66 66 changeset = Changeset.create(:repository => self,
67 67 :revision => revision.identifier,
68 68 :committer => revision.author,
69 69 :committed_on => revision.time,
70 70 :scmid => revision.scmid,
71 71 :comments => revision.message)
72 72
73 73 revision.paths.each do |change|
74 74 Change.create(:changeset => changeset,
75 75 :action => change[:action],
76 76 :path => change[:path],
77 77 :revision => change[:revision])
78 78 end
79 79 end
80 80 end unless revisions.nil?
81 81 identifier_from = identifier_to + 1
82 82 end
83 83 end
84 84 end
85 85 end
86 86 end
@@ -1,150 +1,148
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/cvs_adapter'
19 19 require 'digest/sha1'
20 20
21 21 class Repository::Cvs < Repository
22 22 validates_presence_of :url, :root_url
23 23
24 24 def scm_adapter
25 25 Redmine::Scm::Adapters::CvsAdapter
26 26 end
27 27
28 28 def self.scm_name
29 29 'CVS'
30 30 end
31 31
32 32 def entry(path, identifier)
33 33 e = entries(path, identifier)
34 34 e ? e.first : nil
35 35 end
36 36
37 37 def entries(path=nil, identifier=nil)
38 38 entries=scm.entries(path, identifier)
39 39 if entries
40 40 entries.each() do |entry|
41 41 unless entry.lastrev.nil? || entry.lastrev.identifier
42 42 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 43 if change
44 44 entry.lastrev.identifier=change.changeset.revision
45 45 entry.lastrev.author=change.changeset.committer
46 46 entry.lastrev.revision=change.revision
47 47 entry.lastrev.branch=change.branch
48 48 end
49 49 end
50 50 end
51 51 end
52 52 entries
53 53 end
54 54
55 55 def diff(path, rev, rev_to, type)
56 56 #convert rev to revision. CVS can't handle changesets here
57 57 diff=[]
58 58 changeset_from=changesets.find_by_revision(rev)
59 59 if rev_to.to_i > 0
60 60 changeset_to=changesets.find_by_revision(rev_to)
61 61 end
62 62 changeset_from.changes.each() do |change_from|
63 63
64 64 revision_from=nil
65 65 revision_to=nil
66 66
67 67 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
68 68
69 69 if revision_from
70 70 if changeset_to
71 71 changeset_to.changes.each() do |change_to|
72 72 revision_to=change_to.revision if change_to.path==change_from.path
73 73 end
74 74 end
75 75 unless revision_to
76 76 revision_to=scm.get_previous_revision(revision_from)
77 77 end
78 78 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
79 79 end
80 80 end
81 81 return diff
82 82 end
83 83
84 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 85 # some nifty bits to introduce a commit-id with cvs
89 86 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
90 87 # we now take a guess using the author, the commitlog and the commit-date.
91 88
92 89 # last one is the next step to take. the commit-date is not equal for all
93 90 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
94 91 # we use a small delta here, to merge all changes belonging to _one_ changeset
95 92 time_delta=10.seconds
96 93
94 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
97 95 transaction do
98 scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
96 tmp_rev_num = 1
97 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
99 98 # only add the change to the database, if it doen't exists. the cvs log
100 99 # is not exclusive at all.
101 100 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
102 101 revision
103 102 cs = changesets.find(:first, :conditions=>{
104 103 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 104 :committer=>revision.author,
106 105 :comments=>revision.message
107 106 })
108 107
109 108 # create a new changeset....
110 109 unless cs
111 # we use a negative changeset-number here (just for inserting)
110 # we use a temporaray revision number here (just for inserting)
112 111 # 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
112 latest = changesets.find(:first, :order => 'id DESC')
117 113 cs=Changeset.create(:repository => self,
118 :revision => next_rev,
114 :revision => "_#{tmp_rev_num}",
119 115 :committer => revision.author,
120 116 :committed_on => revision.time,
121 117 :comments => revision.message)
118 tmp_rev_num += 1
122 119 end
123 120
124 121 #convert CVS-File-States to internal Action-abbrevations
125 122 #default action is (M)odified
126 123 action="M"
127 124 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
128 125 action="A" #add-action always at first revision (= 1.1)
129 126 elsif revision.paths[0][:action]=="dead"
130 127 action="D" #dead-state is similar to Delete
131 128 end
132 129
133 130 Change.create(:changeset => cs,
134 131 :action => action,
135 132 :path => scm.with_leading_slash(revision.paths[0][:path]),
136 133 :revision => revision.paths[0][:revision],
137 134 :branch => revision.paths[0][:branch]
138 135 )
139 136 end
140 137 end
141 138
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
139 # Renumber new changesets in chronological order
140 c = changesets.find(:first, :order => 'committed_on DESC, id DESC', :conditions => "revision NOT LIKE '_%'")
141 next_rev = c.nil? ? 1 : (c.revision.to_i + 1)
142 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
143 changeset.update_attribute :revision, next_rev
144 next_rev += 1
148 145 end
146 end # transaction
149 147 end
150 148 end
@@ -1,90 +1,89
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/darcs_adapter'
19 19
20 20 class Repository::Darcs < Repository
21 21 validates_presence_of :url
22 22
23 23 def scm_adapter
24 24 Redmine::Scm::Adapters::DarcsAdapter
25 25 end
26 26
27 27 def self.scm_name
28 28 'Darcs'
29 29 end
30 30
31 31 def entries(path=nil, identifier=nil)
32 32 entries=scm.entries(path, identifier)
33 33 if entries
34 34 entries.each do |entry|
35 35 # Search the DB for the entry's last change
36 36 changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
37 37 if changeset
38 38 entry.lastrev.identifier = changeset.revision
39 39 entry.lastrev.name = changeset.revision
40 40 entry.lastrev.time = changeset.committed_on
41 41 entry.lastrev.author = changeset.committer
42 42 end
43 43 end
44 44 end
45 45 entries
46 46 end
47 47
48 48 def diff(path, rev, rev_to, type)
49 49 patch_from = changesets.find_by_revision(rev)
50 return nil if patch_from.nil?
50 51 patch_to = changesets.find_by_revision(rev_to) if rev_to
51 52 if path.blank?
52 53 path = patch_from.changes.collect{|change| change.path}.join(' ')
53 54 end
54 scm.diff(path, patch_from.scmid, patch_to.scmid, type)
55 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
55 56 end
56 57
57 58 def fetch_changesets
58 59 scm_info = scm.info
59 60 if scm_info
60 61 db_last_id = latest_changeset ? latest_changeset.scmid : nil
61 next_rev = latest_changeset ? latest_changeset.revision + 1 : 1
62 next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
62 63 # latest revision in the repository
63 64 scm_revision = scm_info.lastrev.scmid
64 65 unless changesets.find_by_scmid(scm_revision)
65 66 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
66 67 transaction do
67 68 revisions.reverse_each do |revision|
68 69 changeset = Changeset.create(:repository => self,
69 70 :revision => next_rev,
70 71 :scmid => revision.scmid,
71 72 :committer => revision.author,
72 73 :committed_on => revision.time,
73 74 :comments => revision.message)
74 75
75 next if changeset.new_record?
76
77 76 revision.paths.each do |change|
78 77 Change.create(:changeset => changeset,
79 78 :action => change[:action],
80 79 :path => change[:path],
81 80 :from_path => change[:from_path],
82 81 :from_revision => change[:from_revision])
83 82 end
84 83 next_rev += 1
85 84 end if revisions
86 85 end
87 86 end
88 87 end
89 88 end
90 89 end
@@ -1,74 +1,74
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/subversion_adapter'
19 19
20 20 class Repository::Subversion < Repository
21 21 attr_protected :root_url
22 22 validates_presence_of :url
23 23 validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
24 24
25 25 def scm_adapter
26 26 Redmine::Scm::Adapters::SubversionAdapter
27 27 end
28 28
29 29 def self.scm_name
30 30 'Subversion'
31 31 end
32 32
33 33 def changesets_for_path(path)
34 34 revisions = scm.revisions(path)
35 35 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
36 36 end
37 37
38 38 def fetch_changesets
39 39 scm_info = scm.info
40 40 if scm_info
41 41 # latest revision found in database
42 db_revision = latest_changeset ? latest_changeset.revision : 0
42 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
43 43 # latest revision in the repository
44 44 scm_revision = scm_info.lastrev.identifier.to_i
45 45 if db_revision < scm_revision
46 46 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
47 47 identifier_from = db_revision + 1
48 48 while (identifier_from <= scm_revision)
49 49 # loads changesets by batches of 200
50 50 identifier_to = [identifier_from + 199, scm_revision].min
51 51 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
52 52 transaction do
53 53 revisions.reverse_each do |revision|
54 54 changeset = Changeset.create(:repository => self,
55 55 :revision => revision.identifier,
56 56 :committer => revision.author,
57 57 :committed_on => revision.time,
58 58 :comments => revision.message)
59 59
60 60 revision.paths.each do |change|
61 61 Change.create(:changeset => changeset,
62 62 :action => change[:action],
63 63 :path => change[:path],
64 64 :from_path => change[:from_path],
65 65 :from_revision => change[:from_revision])
66 66 end
67 67 end
68 68 end unless revisions.nil?
69 69 identifier_from = identifier_to + 1
70 70 end
71 71 end
72 72 end
73 73 end
74 74 end
@@ -1,32 +1,32
1 1 <% @entries.each do |entry| %>
2 2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 3 depth = params[:depth].to_i %>
4 4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
5 5 <td class="filename">
6 6 <%= if entry.is_dir?
7 7 link_to_remote h(entry.name),
8 8 {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
9 9 :update => { :success => tr_id },
10 10 :position => :after,
11 11 :success => "scmEntryLoaded('#{tr_id}')",
12 12 :condition => "scmEntryClick('#{tr_id}')"
13 13 },
14 14 {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
15 15 :class => ('icon icon-folder'),
16 16 :style => "margin-left: #{18 * depth}px;"
17 17 }
18 18 else
19 19 link_to h(entry.name),
20 20 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
21 21 :class => 'icon icon-file',
22 22 :style => "margin-left: #{18 * depth}px;"
23 23 end %>
24 24 </td>
25 25 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
26 <td class="revision"><%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
26 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
27 27 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
28 28 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
29 29 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
30 30 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
31 31 </tr>
32 32 <% end %>
@@ -1,28 +1,28
1 1 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %>
2 2 <table class="list changesets">
3 3 <thead><tr>
4 4 <th>#</th>
5 5 <th></th>
6 6 <th></th>
7 7 <th><%= l(:label_date) %></th>
8 8 <th><%= l(:field_author) %></th>
9 9 <th><%= l(:field_comments) %></th>
10 10 </tr></thead>
11 11 <tbody>
12 12 <% show_diff = entry && entry.is_file? && revisions.size > 1 %>
13 13 <% line_num = 1 %>
14 14 <% revisions.each do |changeset| %>
15 15 <tr class="changeset <%= cycle 'odd', 'even' %>">
16 <td class="id"><%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %></td>
16 <td class="id"><%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></td>
17 17 <td class="checkbox"><%= 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 18 <td class="checkbox"><%= 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 19 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
20 20 <td class="author"><%=h changeset.committer.to_s.split('<').first %></td>
21 21 <td class="comments"><%= textilizable(changeset.comments) %></td>
22 22 </tr>
23 23 <% line_num += 1 %>
24 24 <% end %>
25 25 </tbody>
26 26 </table>
27 27 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
28 28 <% end %>
@@ -1,28 +1,28
1 1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
2 2
3 3 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
4 4
5 5 <div class="autoscroll">
6 6 <table class="filecontent annotate CodeRay">
7 7 <tbody>
8 8 <% line_num = 1 %>
9 9 <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
10 10 <% revision = @annotate.revisions[line_num-1] %>
11 11 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
12 12 <th class="line-num"><%= line_num %></th>
13 13 <td class="revision">
14 <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %></td>
14 <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
15 15 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
16 16 <td class="line-code"><pre><%= line %></pre></td>
17 17 </tr>
18 18 <% line_num += 1 %>
19 19 <% end %>
20 20 </tbody>
21 21 </table>
22 22 </div>
23 23
24 24 <% html_title(l(:button_annotate)) -%>
25 25
26 26 <% content_for :header_tags do %>
27 27 <%= stylesheet_link_tag 'scm' %>
28 28 <% end %>
@@ -1,95 +1,95
1 <h2><%= l(:label_revision) %> <%= @rev %>: <%= @path.gsub(/^.*\//, '') %></h2>
1 <h2><%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %></h2>
2 2
3 3 <!-- Choose view type -->
4 4 <% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %>
5 5 <% params.each do |k, p| %>
6 6 <% if k != "type" %>
7 7 <%= hidden_field_tag(k,p) %>
8 8 <% end %>
9 9 <% end %>
10 10 <p><label><%= l(:label_view_diff) %></label>
11 11 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
12 12 <% end %>
13 13
14 14 <% cache(@cache_key) do %>
15 15 <% @diff.each do |table_file| %>
16 16 <div class="autoscroll">
17 17 <% if @diff_type == 'sbs' %>
18 18 <table class="filecontent CodeRay">
19 19 <thead>
20 20 <tr>
21 21 <th colspan="4" class="filename">
22 22 <%= table_file.file_name %>
23 23 </th>
24 24 </tr>
25 25 <tr>
26 <th colspan="2">@<%= @rev %></th>
27 <th colspan="2">@<%= @rev_to %></th>
26 <th colspan="2">@<%= format_revision @rev %></th>
27 <th colspan="2">@<%= format_revision @rev_to %></th>
28 28 </tr>
29 29 </thead>
30 30 <tbody>
31 31 <% table_file.keys.sort.each do |key| %>
32 32 <tr>
33 33 <th class="line-num">
34 34 <%= table_file[key].nb_line_left %>
35 35 </th>
36 36 <td class="line-code <%= table_file[key].type_diff_left %>">
37 37 <pre><%=to_utf8 table_file[key].line_left %></pre>
38 38 </td>
39 39 <th class="line-num">
40 40 <%= table_file[key].nb_line_right %>
41 41 </th>
42 42 <td class="line-code <%= table_file[key].type_diff_right %>">
43 43 <pre><%=to_utf8 table_file[key].line_right %></pre>
44 44 </td>
45 45 </tr>
46 46 <% end %>
47 47 </tbody>
48 48 </table>
49 49
50 50 <% else %>
51 51 <table class="filecontent CodeRay">
52 52 <thead>
53 53 <tr>
54 54 <th colspan="3" class="filename">
55 55 <%= table_file.file_name %>
56 56 </th>
57 57 </tr>
58 58 <tr>
59 <th>@<%= @rev %></th>
60 <th>@<%= @rev_to %></th>
59 <th>@<%= format_revision @rev %></th>
60 <th>@<%= format_revision @rev_to %></th>
61 61 <th></th>
62 62 </tr>
63 63 </thead>
64 64 <tbody>
65 65 <% table_file.keys.sort.each do |key, line| %>
66 66 <tr>
67 67 <th class="line-num">
68 68 <%= table_file[key].nb_line_left %>
69 69 </th>
70 70 <th class="line-num">
71 71 <%= table_file[key].nb_line_right %>
72 72 </th>
73 73 <% if table_file[key].line_left.empty? %>
74 74 <td class="line-code <%= table_file[key].type_diff_right %>">
75 75 <pre><%=to_utf8 table_file[key].line_right %></pre>
76 76 </td>
77 77 <% else %>
78 78 <td class="line-code <%= table_file[key].type_diff_left %>">
79 79 <pre><%=to_utf8 table_file[key].line_left %></pre>
80 80 </td>
81 81 <% end %>
82 82 </tr>
83 83 <% end %>
84 84 </tbody>
85 85 </table>
86 86 <% end %>
87 87 </div>
88 88 <% end %>
89 89 <% end %>
90 90
91 91 <% html_title(with_leading_slash(@path), 'Diff') -%>
92 92
93 93 <% content_for :header_tags do %>
94 94 <%= stylesheet_link_tag "scm" %>
95 95 <% end %>
@@ -1,65 +1,65
1 1 <div class="contextual">
2 2 &#171;
3 3 <% unless @changeset.previous.nil? -%>
4 4 <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %>
5 5 <% else -%>
6 6 <%= l(:label_previous) %>
7 7 <% end -%>
8 8 |
9 9 <% unless @changeset.next.nil? -%>
10 10 <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %>
11 11 <% else -%>
12 12 <%= l(:label_next) %>
13 13 <% end -%>
14 14 &#187;&nbsp;
15 15
16 16 <% form_tag do %>
17 17 <%= text_field_tag 'rev', @rev, :size => 5 %>
18 18 <%= submit_tag 'OK' %>
19 19 <% end %>
20 20 </div>
21 21
22 <h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
22 <h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
23 23
24 24 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
25 25 <em><%= @changeset.committer.to_s.split('<').first %>, <%= format_time(@changeset.committed_on) %></em></p>
26 26
27 27 <%= textilizable @changeset.comments %>
28 28
29 29 <% if @changeset.issues.any? %>
30 30 <h3><%= l(:label_related_issues) %></h3>
31 31 <ul>
32 32 <% @changeset.issues.each do |issue| %>
33 33 <li><%= link_to_issue issue %>: <%=h issue.subject %></li>
34 34 <% end %>
35 35 </ul>
36 36 <% end %>
37 37
38 38 <h3><%= l(:label_attachment_plural) %></h3>
39 39 <div style="float:right;">
40 40 <div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %>&nbsp;</div>
41 41 <div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %>&nbsp;</div>
42 42 <div class="square action_D"></div> <div style="float:left;"><%= l(:label_deleted) %>&nbsp;</div>
43 43 </div>
44 44 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
45 45 <table class="list">
46 46 <tbody>
47 47 <% @changes.each do |change| %>
48 48 <tr class="<%= cycle 'odd', 'even' %>">
49 49 <td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
50 50 <td align="right">
51 51 <% if change.action == "M" %>
52 52 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
53 53 <% end %>
54 54 </td>
55 55 </tr>
56 56 <% end %>
57 57 </tbody>
58 58 </table>
59 59 <p class="pagination"><%= pagination_links_full @changes_pages %></p>
60 60
61 61 <% content_for :header_tags do %>
62 62 <%= stylesheet_link_tag "scm" %>
63 63 <% end %>
64 64
65 65 <% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
@@ -1,21 +1,37
1 1 Creating test repositories
2 2 ===================
3 3
4 4 mkdir tmp/test
5 5
6 6 Subversion
7 7 ----------
8 8 svnadmin create tmp/test/subversion_repository
9 9 gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository
10 10
11 11 CVS
12 12 ---
13 13 gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test
14 14
15 15 Bazaar
16 16 ------
17 17 gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test
18 18
19 19 Mercurial
20 20 ---------
21 21 gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test
22
23 Git
24 ---
25 gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test
26
27
28 Running Tests
29 =============
30
31 Run
32
33 rake --tasks | grep test
34
35 to see available tests.
36
37 RAILS_ENV=test rake test will run tests.
@@ -1,130 +1,130
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/mime_type'
4 4 require 'redmine/themes'
5 5 require 'redmine/plugin'
6 6
7 7 begin
8 8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 9 rescue LoadError
10 10 # RMagick is not available
11 11 end
12 12
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
14 14
15 15 # Permissions
16 16 Redmine::AccessControl.map do |map|
17 17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 18 map.permission :search_project, {:search => :index}, :public => true
19 19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23 23
24 24 map.project_module :issue_tracking do |map|
25 25 # Issue categories
26 26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 27 # Issues
28 28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
29 29 :issues => [:index, :changes, :show, :context_menu],
30 30 :versions => [:show, :status_by],
31 31 :queries => :index,
32 32 :reports => :issue_report}, :public => true
33 33 map.permission :add_issues, {:issues => :new}
34 34 map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
35 35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
36 36 map.permission :add_issue_notes, {:issues => :edit}
37 37 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
38 38 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
39 39 map.permission :move_issues, {:issues => :move}, :require => :loggedin
40 40 map.permission :delete_issues, {:issues => :destroy}, :require => :member
41 41 # Queries
42 42 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
43 43 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
44 44 # Gantt & calendar
45 45 map.permission :view_gantt, :projects => :gantt
46 46 map.permission :view_calendar, :projects => :calendar
47 47 end
48 48
49 49 map.project_module :time_tracking do |map|
50 50 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
51 51 map.permission :view_time_entries, :timelog => [:details, :report]
52 52 end
53 53
54 54 map.project_module :news do |map|
55 55 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
56 56 map.permission :view_news, {:news => [:index, :show]}, :public => true
57 57 map.permission :comment_news, {:news => :add_comment}
58 58 end
59 59
60 60 map.project_module :documents do |map|
61 61 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
62 62 map.permission :view_documents, :documents => [:index, :show, :download]
63 63 end
64 64
65 65 map.project_module :files do |map|
66 66 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
67 67 map.permission :view_files, :projects => :list_files, :versions => :download
68 68 end
69 69
70 70 map.project_module :wiki do |map|
71 71 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
72 72 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
73 73 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
74 74 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
75 75 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
76 76 end
77 77
78 78 map.project_module :repository do |map|
79 79 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
80 80 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
81 81 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
82 82 end
83 83
84 84 map.project_module :boards do |map|
85 85 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
86 86 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
87 87 map.permission :add_messages, {:messages => [:new, :reply]}
88 88 map.permission :edit_messages, {:messages => :edit}, :require => :member
89 89 map.permission :delete_messages, {:messages => :destroy}, :require => :member
90 90 end
91 91 end
92 92
93 93 Redmine::MenuManager.map :top_menu do |menu|
94 94 menu.push :home, :home_url, :html => { :class => 'home' }
95 95 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
96 96 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
97 97 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
98 98 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
99 99 end
100 100
101 101 Redmine::MenuManager.map :account_menu do |menu|
102 102 menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
103 103 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
104 104 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
105 105 menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
106 106 end
107 107
108 108 Redmine::MenuManager.map :application_menu do |menu|
109 109 # Empty
110 110 end
111 111
112 112 Redmine::MenuManager.map :project_menu do |menu|
113 113 menu.push :overview, { :controller => 'projects', :action => 'show' }
114 114 menu.push :activity, { :controller => 'projects', :action => 'activity' }
115 115 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
116 116 :if => Proc.new { |p| p.versions.any? }
117 117 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
118 118 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
119 119 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
120 120 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
121 121 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
122 122 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
123 123 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
124 124 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
125 125 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
126 126 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
127 127 menu.push :repository, { :controller => 'repositories', :action => 'show' },
128 128 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
129 129 menu.push :settings, { :controller => 'projects', :action => 'settings' }
130 130 end
@@ -1,157 +1,161
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19 require 'rexml/document'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class DarcsAdapter < AbstractAdapter
25 25 # Darcs executable name
26 26 DARCS_BIN = "darcs"
27 27
28 28 def initialize(url, root_url=nil, login=nil, password=nil)
29 29 @url = url
30 30 @root_url = url
31 31 end
32 32
33 33 def supports_cat?
34 34 false
35 35 end
36 36
37 37 # Get info about the svn repository
38 38 def info
39 39 rev = revisions(nil,nil,nil,{:limit => 1})
40 40 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
41 41 end
42 42
43 43 # Returns the entry identified by path and revision identifier
44 44 # or nil if entry doesn't exist in the repository
45 45 def entry(path=nil, identifier=nil)
46 46 e = entries(path, identifier)
47 47 e ? e.first : nil
48 48 end
49 49
50 50 # Returns an Entries collection
51 51 # or nil if the given path doesn't exist in the repository
52 52 def entries(path=nil, identifier=nil)
53 53 path_prefix = (path.blank? ? '' : "#{path}/")
54 54 path = '.' if path.blank?
55 55 entries = Entries.new
56 56 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output #{path}"
57 57 shellout(cmd) do |io|
58 58 begin
59 59 doc = REXML::Document.new(io)
60 60 if doc.root.name == 'directory'
61 61 doc.elements.each('directory/*') do |element|
62 62 next unless ['file', 'directory'].include? element.name
63 63 entries << entry_from_xml(element, path_prefix)
64 64 end
65 65 elsif doc.root.name == 'file'
66 66 entries << entry_from_xml(doc.root, path_prefix)
67 67 end
68 68 rescue
69 69 end
70 70 end
71 71 return nil if $? && $?.exitstatus != 0
72 72 entries.sort_by_name
73 73 end
74 74
75 75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 76 path = '.' if path.blank?
77 77 revisions = Revisions.new
78 78 cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
79 79 cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
80 80 cmd << " --last #{options[:limit].to_i}" if options[:limit]
81 81 shellout(cmd) do |io|
82 82 begin
83 83 doc = REXML::Document.new(io)
84 84 doc.elements.each("changelog/patch") do |patch|
85 85 message = patch.elements['name'].text
86 86 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
87 87 revisions << Revision.new({:identifier => nil,
88 88 :author => patch.attributes['author'],
89 89 :scmid => patch.attributes['hash'],
90 90 :time => Time.parse(patch.attributes['local_date']),
91 91 :message => message,
92 92 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
93 93 })
94 94 end
95 95 rescue
96 96 end
97 97 end
98 98 return nil if $? && $?.exitstatus != 0
99 99 revisions
100 100 end
101 101
102 102 def diff(path, identifier_from, identifier_to=nil, type="inline")
103 103 path = '*' if path.blank?
104 104 cmd = "#{DARCS_BIN} diff --repodir #{@url}"
105 if identifier_to.nil?
106 cmd << " --match \"hash #{identifier_from}\""
107 else
105 108 cmd << " --to-match \"hash #{identifier_from}\""
106 cmd << " --from-match \"hash #{identifier_to}\"" if identifier_to
109 cmd << " --from-match \"hash #{identifier_to}\""
110 end
107 111 cmd << " -u #{path}"
108 112 diff = []
109 113 shellout(cmd) do |io|
110 114 io.each_line do |line|
111 115 diff << line
112 116 end
113 117 end
114 118 return nil if $? && $?.exitstatus != 0
115 119 DiffTableList.new diff, type
116 120 end
117 121
118 122 private
119 123
120 124 def entry_from_xml(element, path_prefix)
121 125 Entry.new({:name => element.attributes['name'],
122 126 :path => path_prefix + element.attributes['name'],
123 127 :kind => element.name == 'file' ? 'file' : 'dir',
124 128 :size => nil,
125 129 :lastrev => Revision.new({
126 130 :identifier => nil,
127 131 :scmid => element.elements['modified'].elements['patch'].attributes['hash']
128 132 })
129 133 })
130 134 end
131 135
132 136 # Retrieve changed paths for a single patch
133 137 def get_paths_for_patch(hash)
134 138 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
135 139 cmd << " --match \"hash #{hash}\" "
136 140 paths = []
137 141 shellout(cmd) do |io|
138 142 begin
139 143 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
140 144 # A root element is added so that REXML doesn't raise an error
141 145 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
142 146 doc.elements.each('fake_root/summary/*') do |modif|
143 147 paths << {:action => modif.name[0,1].upcase,
144 148 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
145 149 }
146 150 end
147 151 rescue
148 152 end
149 153 end
150 154 paths
151 155 rescue CommandFailed
152 156 paths
153 157 end
154 158 end
155 159 end
156 160 end
157 161 end
@@ -1,60 +1,60
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'pp'
20 20 class RepositoryCvsTest < Test::Unit::TestCase
21 21 fixtures :projects
22 22
23 23 # No '..' in the repository path
24 24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
25 25 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
26 26 # CVS module
27 27 MODULE_NAME = 'test'
28 28
29 29 def setup
30 30 @project = Project.find(1)
31 31 assert @repository = Repository::Cvs.create(:project => @project,
32 32 :root_url => REPOSITORY_PATH,
33 33 :url => MODULE_NAME)
34 34 end
35 35
36 36 if File.directory?(REPOSITORY_PATH)
37 37 def test_fetch_changesets_from_scratch
38 38 @repository.fetch_changesets
39 39 @repository.reload
40 40
41 41 assert_equal 5, @repository.changesets.count
42 42 assert_equal 14, @repository.changes.count
43 assert_equal 'Two files changed', @repository.changesets.find_by_revision(3).comments
43 assert_not_nil @repository.changesets.find_by_comments('Two files changed')
44 44 end
45 45
46 46 def test_fetch_changesets_incremental
47 47 @repository.fetch_changesets
48 # Remove changesets with revision > 2
49 @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy)
48 # Remove the 3 latest changesets
49 @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
50 50 @repository.reload
51 51 assert_equal 2, @repository.changesets.count
52 52
53 53 @repository.fetch_changesets
54 54 assert_equal 5, @repository.changesets.count
55 55 end
56 56 else
57 57 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
58 58 def test_fake; assert true end
59 59 end
60 60 end
General Comments 0
You need to be logged in to leave comments. Login now