##// END OF EJS Templates
scm: Ruby 1.9 compatibility in getting scm version (#4273)....
Toshi MARUYAMA -
r4800:11e4c5c1ea1f
parent child
Show More
@@ -1,218 +1,221
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class BazaarAdapter < AbstractAdapter
23 class BazaarAdapter < AbstractAdapter
24
24
25 # Bazaar executable name
25 # Bazaar executable name
26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27
27
28 class << self
28 class << self
29 def client_command
29 def client_command
30 @@bin ||= BZR_BIN
30 @@bin ||= BZR_BIN
31 end
31 end
32
32
33 def sq_bin
33 def sq_bin
34 @@sq_bin ||= shell_quote(BZR_BIN)
34 @@sq_bin ||= shell_quote(BZR_BIN)
35 end
35 end
36
36
37 def client_version
37 def client_version
38 @@client_version ||= (scm_command_version || [])
38 @@client_version ||= (scm_command_version || [])
39 end
39 end
40
40
41 def client_available
41 def client_available
42 !client_version.empty?
42 !client_version.empty?
43 end
43 end
44
44
45 def scm_command_version
45 def scm_command_version
46 scm_version = scm_version_from_command_line
46 scm_version = scm_version_from_command_line
47 if scm_version.respond_to?(:force_encoding)
48 scm_version.force_encoding('ASCII-8BIT')
49 end
47 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
50 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
48 m[2].scan(%r{\d+}).collect(&:to_i)
51 m[2].scan(%r{\d+}).collect(&:to_i)
49 end
52 end
50 end
53 end
51
54
52 def scm_version_from_command_line
55 def scm_version_from_command_line
53 shellout("#{sq_bin} --version") { |io| io.read }.to_s
56 shellout("#{sq_bin} --version") { |io| io.read }.to_s
54 end
57 end
55 end
58 end
56
59
57 # Get info about the repository
60 # Get info about the repository
58 def info
61 def info
59 cmd = "#{self.class.sq_bin} revno #{target('')}"
62 cmd = "#{self.class.sq_bin} revno #{target('')}"
60 info = nil
63 info = nil
61 shellout(cmd) do |io|
64 shellout(cmd) do |io|
62 if io.read =~ %r{^(\d+)\r?$}
65 if io.read =~ %r{^(\d+)\r?$}
63 info = Info.new({:root_url => url,
66 info = Info.new({:root_url => url,
64 :lastrev => Revision.new({
67 :lastrev => Revision.new({
65 :identifier => $1
68 :identifier => $1
66 })
69 })
67 })
70 })
68 end
71 end
69 end
72 end
70 return nil if $? && $?.exitstatus != 0
73 return nil if $? && $?.exitstatus != 0
71 info
74 info
72 rescue CommandFailed
75 rescue CommandFailed
73 return nil
76 return nil
74 end
77 end
75
78
76 # Returns an Entries collection
79 # Returns an Entries collection
77 # or nil if the given path doesn't exist in the repository
80 # or nil if the given path doesn't exist in the repository
78 def entries(path=nil, identifier=nil)
81 def entries(path=nil, identifier=nil)
79 path ||= ''
82 path ||= ''
80 entries = Entries.new
83 entries = Entries.new
81 cmd = "#{self.class.sq_bin} ls -v --show-ids"
84 cmd = "#{self.class.sq_bin} ls -v --show-ids"
82 identifier = -1 unless identifier && identifier.to_i > 0
85 identifier = -1 unless identifier && identifier.to_i > 0
83 cmd << " -r#{identifier.to_i}"
86 cmd << " -r#{identifier.to_i}"
84 cmd << " #{target(path)}"
87 cmd << " #{target(path)}"
85 shellout(cmd) do |io|
88 shellout(cmd) do |io|
86 prefix = "#{url}/#{path}".gsub('\\', '/')
89 prefix = "#{url}/#{path}".gsub('\\', '/')
87 logger.debug "PREFIX: #{prefix}"
90 logger.debug "PREFIX: #{prefix}"
88 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
91 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
89 io.each_line do |line|
92 io.each_line do |line|
90 next unless line =~ re
93 next unless line =~ re
91 entries << Entry.new({:name => $3.strip,
94 entries << Entry.new({:name => $3.strip,
92 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
95 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
93 :kind => ($4.blank? ? 'file' : 'dir'),
96 :kind => ($4.blank? ? 'file' : 'dir'),
94 :size => nil,
97 :size => nil,
95 :lastrev => Revision.new(:revision => $5.strip)
98 :lastrev => Revision.new(:revision => $5.strip)
96 })
99 })
97 end
100 end
98 end
101 end
99 return nil if $? && $?.exitstatus != 0
102 return nil if $? && $?.exitstatus != 0
100 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
103 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
101 entries.sort_by_name
104 entries.sort_by_name
102 end
105 end
103
106
104 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
105 path ||= ''
108 path ||= ''
106 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
109 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
107 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
110 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
108 revisions = Revisions.new
111 revisions = Revisions.new
109 cmd = "#{self.class.sq_bin} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
112 cmd = "#{self.class.sq_bin} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
110 shellout(cmd) do |io|
113 shellout(cmd) do |io|
111 revision = nil
114 revision = nil
112 parsing = nil
115 parsing = nil
113 io.each_line do |line|
116 io.each_line do |line|
114 if line =~ /^----/
117 if line =~ /^----/
115 revisions << revision if revision
118 revisions << revision if revision
116 revision = Revision.new(:paths => [], :message => '')
119 revision = Revision.new(:paths => [], :message => '')
117 parsing = nil
120 parsing = nil
118 else
121 else
119 next unless revision
122 next unless revision
120
123
121 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
124 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
122 revision.identifier = $1.to_i
125 revision.identifier = $1.to_i
123 elsif line =~ /^committer: (.+)$/
126 elsif line =~ /^committer: (.+)$/
124 revision.author = $1.strip
127 revision.author = $1.strip
125 elsif line =~ /^revision-id:(.+)$/
128 elsif line =~ /^revision-id:(.+)$/
126 revision.scmid = $1.strip
129 revision.scmid = $1.strip
127 elsif line =~ /^timestamp: (.+)$/
130 elsif line =~ /^timestamp: (.+)$/
128 revision.time = Time.parse($1).localtime
131 revision.time = Time.parse($1).localtime
129 elsif line =~ /^ -----/
132 elsif line =~ /^ -----/
130 # partial revisions
133 # partial revisions
131 parsing = nil unless parsing == 'message'
134 parsing = nil unless parsing == 'message'
132 elsif line =~ /^(message|added|modified|removed|renamed):/
135 elsif line =~ /^(message|added|modified|removed|renamed):/
133 parsing = $1
136 parsing = $1
134 elsif line =~ /^ (.*)$/
137 elsif line =~ /^ (.*)$/
135 if parsing == 'message'
138 if parsing == 'message'
136 revision.message << "#{$1}\n"
139 revision.message << "#{$1}\n"
137 else
140 else
138 if $1 =~ /^(.*)\s+(\S+)$/
141 if $1 =~ /^(.*)\s+(\S+)$/
139 path = $1.strip
142 path = $1.strip
140 revid = $2
143 revid = $2
141 case parsing
144 case parsing
142 when 'added'
145 when 'added'
143 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
146 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
144 when 'modified'
147 when 'modified'
145 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
148 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
146 when 'removed'
149 when 'removed'
147 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
150 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
148 when 'renamed'
151 when 'renamed'
149 new_path = path.split('=>').last
152 new_path = path.split('=>').last
150 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
153 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
151 end
154 end
152 end
155 end
153 end
156 end
154 else
157 else
155 parsing = nil
158 parsing = nil
156 end
159 end
157 end
160 end
158 end
161 end
159 revisions << revision if revision
162 revisions << revision if revision
160 end
163 end
161 return nil if $? && $?.exitstatus != 0
164 return nil if $? && $?.exitstatus != 0
162 revisions
165 revisions
163 end
166 end
164
167
165 def diff(path, identifier_from, identifier_to=nil)
168 def diff(path, identifier_from, identifier_to=nil)
166 path ||= ''
169 path ||= ''
167 if identifier_to
170 if identifier_to
168 identifier_to = identifier_to.to_i
171 identifier_to = identifier_to.to_i
169 else
172 else
170 identifier_to = identifier_from.to_i - 1
173 identifier_to = identifier_from.to_i - 1
171 end
174 end
172 if identifier_from
175 if identifier_from
173 identifier_from = identifier_from.to_i
176 identifier_from = identifier_from.to_i
174 end
177 end
175 cmd = "#{self.class.sq_bin} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
178 cmd = "#{self.class.sq_bin} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
176 diff = []
179 diff = []
177 shellout(cmd) do |io|
180 shellout(cmd) do |io|
178 io.each_line do |line|
181 io.each_line do |line|
179 diff << line
182 diff << line
180 end
183 end
181 end
184 end
182 #return nil if $? && $?.exitstatus != 0
185 #return nil if $? && $?.exitstatus != 0
183 diff
186 diff
184 end
187 end
185
188
186 def cat(path, identifier=nil)
189 def cat(path, identifier=nil)
187 cmd = "#{self.class.sq_bin} cat"
190 cmd = "#{self.class.sq_bin} cat"
188 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
191 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
189 cmd << " #{target(path)}"
192 cmd << " #{target(path)}"
190 cat = nil
193 cat = nil
191 shellout(cmd) do |io|
194 shellout(cmd) do |io|
192 io.binmode
195 io.binmode
193 cat = io.read
196 cat = io.read
194 end
197 end
195 return nil if $? && $?.exitstatus != 0
198 return nil if $? && $?.exitstatus != 0
196 cat
199 cat
197 end
200 end
198
201
199 def annotate(path, identifier=nil)
202 def annotate(path, identifier=nil)
200 cmd = "#{self.class.sq_bin} annotate --all"
203 cmd = "#{self.class.sq_bin} annotate --all"
201 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
204 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
202 cmd << " #{target(path)}"
205 cmd << " #{target(path)}"
203 blame = Annotate.new
206 blame = Annotate.new
204 shellout(cmd) do |io|
207 shellout(cmd) do |io|
205 author = nil
208 author = nil
206 identifier = nil
209 identifier = nil
207 io.each_line do |line|
210 io.each_line do |line|
208 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
211 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
209 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
212 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
210 end
213 end
211 end
214 end
212 return nil if $? && $?.exitstatus != 0
215 return nil if $? && $?.exitstatus != 0
213 blame
216 blame
214 end
217 end
215 end
218 end
216 end
219 end
217 end
220 end
218 end
221 end
@@ -1,400 +1,403
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CvsAdapter < AbstractAdapter
23 class CvsAdapter < AbstractAdapter
24
24
25 # CVS executable name
25 # CVS executable name
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
27
27
28 class << self
28 class << self
29 def client_command
29 def client_command
30 @@bin ||= CVS_BIN
30 @@bin ||= CVS_BIN
31 end
31 end
32
32
33 def sq_bin
33 def sq_bin
34 @@sq_bin ||= shell_quote(CVS_BIN)
34 @@sq_bin ||= shell_quote(CVS_BIN)
35 end
35 end
36
36
37 def client_version
37 def client_version
38 @@client_version ||= (scm_command_version || [])
38 @@client_version ||= (scm_command_version || [])
39 end
39 end
40
40
41 def client_available
41 def client_available
42 client_version_above?([1, 12])
42 client_version_above?([1, 12])
43 end
43 end
44
44
45 def scm_command_version
45 def scm_command_version
46 scm_version = scm_version_from_command_line
46 scm_version = scm_version_from_command_line
47 if scm_version.respond_to?(:force_encoding)
48 scm_version.force_encoding('ASCII-8BIT')
49 end
47 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
50 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
48 m[2].scan(%r{\d+}).collect(&:to_i)
51 m[2].scan(%r{\d+}).collect(&:to_i)
49 end
52 end
50 end
53 end
51
54
52 def scm_version_from_command_line
55 def scm_version_from_command_line
53 shellout("#{sq_bin} --version") { |io| io.read }.to_s
56 shellout("#{sq_bin} --version") { |io| io.read }.to_s
54 end
57 end
55 end
58 end
56
59
57 # Guidelines for the input:
60 # Guidelines for the input:
58 # url -> the project-path, relative to the cvsroot (eg. module name)
61 # url -> the project-path, relative to the cvsroot (eg. module name)
59 # root_url -> the good old, sometimes damned, CVSROOT
62 # root_url -> the good old, sometimes damned, CVSROOT
60 # login -> unnecessary
63 # login -> unnecessary
61 # password -> unnecessary too
64 # password -> unnecessary too
62 def initialize(url, root_url=nil, login=nil, password=nil)
65 def initialize(url, root_url=nil, login=nil, password=nil)
63 @url = url
66 @url = url
64 @login = login if login && !login.empty?
67 @login = login if login && !login.empty?
65 @password = (password || "") if @login
68 @password = (password || "") if @login
66 #TODO: better Exception here (IllegalArgumentException)
69 #TODO: better Exception here (IllegalArgumentException)
67 raise CommandFailed if root_url.blank?
70 raise CommandFailed if root_url.blank?
68 @root_url = root_url
71 @root_url = root_url
69 end
72 end
70
73
71 def root_url
74 def root_url
72 @root_url
75 @root_url
73 end
76 end
74
77
75 def url
78 def url
76 @url
79 @url
77 end
80 end
78
81
79 def info
82 def info
80 logger.debug "<cvs> info"
83 logger.debug "<cvs> info"
81 Info.new({:root_url => @root_url, :lastrev => nil})
84 Info.new({:root_url => @root_url, :lastrev => nil})
82 end
85 end
83
86
84 def get_previous_revision(revision)
87 def get_previous_revision(revision)
85 CvsRevisionHelper.new(revision).prevRev
88 CvsRevisionHelper.new(revision).prevRev
86 end
89 end
87
90
88 # Returns an Entries collection
91 # Returns an Entries collection
89 # or nil if the given path doesn't exist in the repository
92 # or nil if the given path doesn't exist in the repository
90 # this method is used by the repository-browser (aka LIST)
93 # this method is used by the repository-browser (aka LIST)
91 def entries(path=nil, identifier=nil)
94 def entries(path=nil, identifier=nil)
92 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
95 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
93 path_with_project="#{url}#{with_leading_slash(path)}"
96 path_with_project="#{url}#{with_leading_slash(path)}"
94 entries = Entries.new
97 entries = Entries.new
95 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rls -e"
98 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rls -e"
96 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
99 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
97 cmd << " #{shell_quote path_with_project}"
100 cmd << " #{shell_quote path_with_project}"
98 shellout(cmd) do |io|
101 shellout(cmd) do |io|
99 io.each_line(){|line|
102 io.each_line(){|line|
100 fields=line.chop.split('/',-1)
103 fields=line.chop.split('/',-1)
101 logger.debug(">>InspectLine #{fields.inspect}")
104 logger.debug(">>InspectLine #{fields.inspect}")
102
105
103 if fields[0]!="D"
106 if fields[0]!="D"
104 entries << Entry.new({:name => fields[-5],
107 entries << Entry.new({:name => fields[-5],
105 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
108 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
106 :path => "#{path}/#{fields[-5]}",
109 :path => "#{path}/#{fields[-5]}",
107 :kind => 'file',
110 :kind => 'file',
108 :size => nil,
111 :size => nil,
109 :lastrev => Revision.new({
112 :lastrev => Revision.new({
110 :revision => fields[-4],
113 :revision => fields[-4],
111 :name => fields[-4],
114 :name => fields[-4],
112 :time => Time.parse(fields[-3]),
115 :time => Time.parse(fields[-3]),
113 :author => ''
116 :author => ''
114 })
117 })
115 })
118 })
116 else
119 else
117 entries << Entry.new({:name => fields[1],
120 entries << Entry.new({:name => fields[1],
118 :path => "#{path}/#{fields[1]}",
121 :path => "#{path}/#{fields[1]}",
119 :kind => 'dir',
122 :kind => 'dir',
120 :size => nil,
123 :size => nil,
121 :lastrev => nil
124 :lastrev => nil
122 })
125 })
123 end
126 end
124 }
127 }
125 end
128 end
126 return nil if $? && $?.exitstatus != 0
129 return nil if $? && $?.exitstatus != 0
127 entries.sort_by_name
130 entries.sort_by_name
128 end
131 end
129
132
130 STARTLOG="----------------------------"
133 STARTLOG="----------------------------"
131 ENDLOG ="============================================================================="
134 ENDLOG ="============================================================================="
132
135
133 # Returns all revisions found between identifier_from and identifier_to
136 # Returns all revisions found between identifier_from and identifier_to
134 # in the repository. both identifier have to be dates or nil.
137 # in the repository. both identifier have to be dates or nil.
135 # these method returns nothing but yield every result in block
138 # these method returns nothing but yield every result in block
136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
139 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
137 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
140 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
138
141
139 path_with_project="#{url}#{with_leading_slash(path)}"
142 path_with_project="#{url}#{with_leading_slash(path)}"
140 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
143 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
141 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
144 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
142 cmd << " #{shell_quote path_with_project}"
145 cmd << " #{shell_quote path_with_project}"
143 shellout(cmd) do |io|
146 shellout(cmd) do |io|
144 state="entry_start"
147 state="entry_start"
145
148
146 commit_log=String.new
149 commit_log=String.new
147 revision=nil
150 revision=nil
148 date=nil
151 date=nil
149 author=nil
152 author=nil
150 entry_path=nil
153 entry_path=nil
151 entry_name=nil
154 entry_name=nil
152 file_state=nil
155 file_state=nil
153 branch_map=nil
156 branch_map=nil
154
157
155 io.each_line() do |line|
158 io.each_line() do |line|
156
159
157 if state!="revision" && /^#{ENDLOG}/ =~ line
160 if state!="revision" && /^#{ENDLOG}/ =~ line
158 commit_log=String.new
161 commit_log=String.new
159 revision=nil
162 revision=nil
160 state="entry_start"
163 state="entry_start"
161 end
164 end
162
165
163 if state=="entry_start"
166 if state=="entry_start"
164 branch_map=Hash.new
167 branch_map=Hash.new
165 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
168 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
166 entry_path = normalize_cvs_path($1)
169 entry_path = normalize_cvs_path($1)
167 entry_name = normalize_path(File.basename($1))
170 entry_name = normalize_path(File.basename($1))
168 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
171 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
169 elsif /^head: (.+)$/ =~ line
172 elsif /^head: (.+)$/ =~ line
170 entry_headRev = $1 #unless entry.nil?
173 entry_headRev = $1 #unless entry.nil?
171 elsif /^symbolic names:/ =~ line
174 elsif /^symbolic names:/ =~ line
172 state="symbolic" #unless entry.nil?
175 state="symbolic" #unless entry.nil?
173 elsif /^#{STARTLOG}/ =~ line
176 elsif /^#{STARTLOG}/ =~ line
174 commit_log=String.new
177 commit_log=String.new
175 state="revision"
178 state="revision"
176 end
179 end
177 next
180 next
178 elsif state=="symbolic"
181 elsif state=="symbolic"
179 if /^(.*):\s(.*)/ =~ (line.strip)
182 if /^(.*):\s(.*)/ =~ (line.strip)
180 branch_map[$1]=$2
183 branch_map[$1]=$2
181 else
184 else
182 state="tags"
185 state="tags"
183 next
186 next
184 end
187 end
185 elsif state=="tags"
188 elsif state=="tags"
186 if /^#{STARTLOG}/ =~ line
189 if /^#{STARTLOG}/ =~ line
187 commit_log = ""
190 commit_log = ""
188 state="revision"
191 state="revision"
189 elsif /^#{ENDLOG}/ =~ line
192 elsif /^#{ENDLOG}/ =~ line
190 state="head"
193 state="head"
191 end
194 end
192 next
195 next
193 elsif state=="revision"
196 elsif state=="revision"
194 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
197 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
195 if revision
198 if revision
196
199
197 revHelper=CvsRevisionHelper.new(revision)
200 revHelper=CvsRevisionHelper.new(revision)
198 revBranch="HEAD"
201 revBranch="HEAD"
199
202
200 branch_map.each() do |branch_name,branch_point|
203 branch_map.each() do |branch_name,branch_point|
201 if revHelper.is_in_branch_with_symbol(branch_point)
204 if revHelper.is_in_branch_with_symbol(branch_point)
202 revBranch=branch_name
205 revBranch=branch_name
203 end
206 end
204 end
207 end
205
208
206 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
209 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
207
210
208 yield Revision.new({
211 yield Revision.new({
209 :time => date,
212 :time => date,
210 :author => author,
213 :author => author,
211 :message=>commit_log.chomp,
214 :message=>commit_log.chomp,
212 :paths => [{
215 :paths => [{
213 :revision => revision,
216 :revision => revision,
214 :branch=> revBranch,
217 :branch=> revBranch,
215 :path=>entry_path,
218 :path=>entry_path,
216 :name=>entry_name,
219 :name=>entry_name,
217 :kind=>'file',
220 :kind=>'file',
218 :action=>file_state
221 :action=>file_state
219 }]
222 }]
220 })
223 })
221 end
224 end
222
225
223 commit_log=String.new
226 commit_log=String.new
224 revision=nil
227 revision=nil
225
228
226 if /^#{ENDLOG}/ =~ line
229 if /^#{ENDLOG}/ =~ line
227 state="entry_start"
230 state="entry_start"
228 end
231 end
229 next
232 next
230 end
233 end
231
234
232 if /^branches: (.+)$/ =~ line
235 if /^branches: (.+)$/ =~ line
233 #TODO: version.branch = $1
236 #TODO: version.branch = $1
234 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
237 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
235 revision = $1
238 revision = $1
236 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
239 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
237 date = Time.parse($1)
240 date = Time.parse($1)
238 author = /author: ([^;]+)/.match(line)[1]
241 author = /author: ([^;]+)/.match(line)[1]
239 file_state = /state: ([^;]+)/.match(line)[1]
242 file_state = /state: ([^;]+)/.match(line)[1]
240 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
243 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
241 # useful for stats or something else
244 # useful for stats or something else
242 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
245 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
243 # unless linechanges.nil?
246 # unless linechanges.nil?
244 # version.line_plus = linechanges[1]
247 # version.line_plus = linechanges[1]
245 # version.line_minus = linechanges[2]
248 # version.line_minus = linechanges[2]
246 # else
249 # else
247 # version.line_plus = 0
250 # version.line_plus = 0
248 # version.line_minus = 0
251 # version.line_minus = 0
249 # end
252 # end
250 else
253 else
251 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
254 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
252 end
255 end
253 end
256 end
254 end
257 end
255 end
258 end
256 end
259 end
257
260
258 def diff(path, identifier_from, identifier_to=nil)
261 def diff(path, identifier_from, identifier_to=nil)
259 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
262 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
260 path_with_project="#{url}#{with_leading_slash(path)}"
263 path_with_project="#{url}#{with_leading_slash(path)}"
261 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
264 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
262 diff = []
265 diff = []
263 shellout(cmd) do |io|
266 shellout(cmd) do |io|
264 io.each_line do |line|
267 io.each_line do |line|
265 diff << line
268 diff << line
266 end
269 end
267 end
270 end
268 return nil if $? && $?.exitstatus != 0
271 return nil if $? && $?.exitstatus != 0
269 diff
272 diff
270 end
273 end
271
274
272 def cat(path, identifier=nil)
275 def cat(path, identifier=nil)
273 identifier = (identifier) ? identifier : "HEAD"
276 identifier = (identifier) ? identifier : "HEAD"
274 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
277 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
275 path_with_project="#{url}#{with_leading_slash(path)}"
278 path_with_project="#{url}#{with_leading_slash(path)}"
276 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
279 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
277 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
280 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
278 cmd << " -p #{shell_quote path_with_project}"
281 cmd << " -p #{shell_quote path_with_project}"
279 cat = nil
282 cat = nil
280 shellout(cmd) do |io|
283 shellout(cmd) do |io|
281 io.binmode
284 io.binmode
282 cat = io.read
285 cat = io.read
283 end
286 end
284 return nil if $? && $?.exitstatus != 0
287 return nil if $? && $?.exitstatus != 0
285 cat
288 cat
286 end
289 end
287
290
288 def annotate(path, identifier=nil)
291 def annotate(path, identifier=nil)
289 identifier = (identifier) ? identifier.to_i : "HEAD"
292 identifier = (identifier) ? identifier.to_i : "HEAD"
290 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
293 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
291 path_with_project="#{url}#{with_leading_slash(path)}"
294 path_with_project="#{url}#{with_leading_slash(path)}"
292 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
295 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
293 blame = Annotate.new
296 blame = Annotate.new
294 shellout(cmd) do |io|
297 shellout(cmd) do |io|
295 io.each_line do |line|
298 io.each_line do |line|
296 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
299 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
297 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
300 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
298 end
301 end
299 end
302 end
300 return nil if $? && $?.exitstatus != 0
303 return nil if $? && $?.exitstatus != 0
301 blame
304 blame
302 end
305 end
303
306
304 private
307 private
305
308
306 # Returns the root url without the connexion string
309 # Returns the root url without the connexion string
307 # :pserver:anonymous@foo.bar:/path => /path
310 # :pserver:anonymous@foo.bar:/path => /path
308 # :ext:cvsservername:/path => /path
311 # :ext:cvsservername:/path => /path
309 def root_url_path
312 def root_url_path
310 root_url.to_s.gsub(/^:.+:\d*/, '')
313 root_url.to_s.gsub(/^:.+:\d*/, '')
311 end
314 end
312
315
313 # convert a date/time into the CVS-format
316 # convert a date/time into the CVS-format
314 def time_to_cvstime(time)
317 def time_to_cvstime(time)
315 return nil if time.nil?
318 return nil if time.nil?
316 return Time.now if time == 'HEAD'
319 return Time.now if time == 'HEAD'
317
320
318 unless time.kind_of? Time
321 unless time.kind_of? Time
319 time = Time.parse(time)
322 time = Time.parse(time)
320 end
323 end
321 return time.strftime("%Y-%m-%d %H:%M:%S")
324 return time.strftime("%Y-%m-%d %H:%M:%S")
322 end
325 end
323
326
324 def time_to_cvstime_rlog(time)
327 def time_to_cvstime_rlog(time)
325 return nil if time.nil?
328 return nil if time.nil?
326 t1 = time.clone.localtime
329 t1 = time.clone.localtime
327 return t1.strftime("%Y-%m-%d %H:%M:%S")
330 return t1.strftime("%Y-%m-%d %H:%M:%S")
328 end
331 end
329
332
330 def normalize_cvs_path(path)
333 def normalize_cvs_path(path)
331 normalize_path(path.gsub(/Attic\//,''))
334 normalize_path(path.gsub(/Attic\//,''))
332 end
335 end
333
336
334 def normalize_path(path)
337 def normalize_path(path)
335 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
338 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
336 end
339 end
337 end
340 end
338
341
339 class CvsRevisionHelper
342 class CvsRevisionHelper
340 attr_accessor :complete_rev, :revision, :base, :branchid
343 attr_accessor :complete_rev, :revision, :base, :branchid
341
344
342 def initialize(complete_rev)
345 def initialize(complete_rev)
343 @complete_rev = complete_rev
346 @complete_rev = complete_rev
344 parseRevision()
347 parseRevision()
345 end
348 end
346
349
347 def branchPoint
350 def branchPoint
348 return @base
351 return @base
349 end
352 end
350
353
351 def branchVersion
354 def branchVersion
352 if isBranchRevision
355 if isBranchRevision
353 return @base+"."+@branchid
356 return @base+"."+@branchid
354 end
357 end
355 return @base
358 return @base
356 end
359 end
357
360
358 def isBranchRevision
361 def isBranchRevision
359 !@branchid.nil?
362 !@branchid.nil?
360 end
363 end
361
364
362 def prevRev
365 def prevRev
363 unless @revision==0
366 unless @revision==0
364 return buildRevision(@revision-1)
367 return buildRevision(@revision-1)
365 end
368 end
366 return buildRevision(@revision)
369 return buildRevision(@revision)
367 end
370 end
368
371
369 def is_in_branch_with_symbol(branch_symbol)
372 def is_in_branch_with_symbol(branch_symbol)
370 bpieces=branch_symbol.split(".")
373 bpieces=branch_symbol.split(".")
371 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
374 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
372 return (branchVersion==branch_start)
375 return (branchVersion==branch_start)
373 end
376 end
374
377
375 private
378 private
376 def buildRevision(rev)
379 def buildRevision(rev)
377 if rev== 0
380 if rev== 0
378 @base
381 @base
379 elsif @branchid.nil?
382 elsif @branchid.nil?
380 @base+"."+rev.to_s
383 @base+"."+rev.to_s
381 else
384 else
382 @base+"."+@branchid+"."+rev.to_s
385 @base+"."+@branchid+"."+rev.to_s
383 end
386 end
384 end
387 end
385
388
386 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
389 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
387 def parseRevision()
390 def parseRevision()
388 pieces=@complete_rev.split(".")
391 pieces=@complete_rev.split(".")
389 @revision=pieces.last.to_i
392 @revision=pieces.last.to_i
390 baseSize=1
393 baseSize=1
391 baseSize+=(pieces.size/2)
394 baseSize+=(pieces.size/2)
392 @base=pieces[0..-baseSize].join(".")
395 @base=pieces[0..-baseSize].join(".")
393 if baseSize > 2
396 if baseSize > 2
394 @branchid=pieces[-2]
397 @branchid=pieces[-2]
395 end
398 end
396 end
399 end
397 end
400 end
398 end
401 end
399 end
402 end
400 end
403 end
@@ -1,237 +1,240
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'rexml/document'
19 require 'rexml/document'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class DarcsAdapter < AbstractAdapter
24 class DarcsAdapter < AbstractAdapter
25 # Darcs executable name
25 # Darcs executable name
26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
27
27
28 class << self
28 class << self
29 def client_command
29 def client_command
30 @@bin ||= DARCS_BIN
30 @@bin ||= DARCS_BIN
31 end
31 end
32
32
33 def sq_bin
33 def sq_bin
34 @@sq_bin ||= shell_quote(DARCS_BIN)
34 @@sq_bin ||= shell_quote(DARCS_BIN)
35 end
35 end
36
36
37 def client_version
37 def client_version
38 @@client_version ||= (darcs_binary_version || [])
38 @@client_version ||= (darcs_binary_version || [])
39 end
39 end
40
40
41 def client_available
41 def client_available
42 !client_version.empty?
42 !client_version.empty?
43 end
43 end
44
44
45 def darcs_binary_version
45 def darcs_binary_version
46 darcsversion = darcs_binary_version_from_command_line
46 darcsversion = darcs_binary_version_from_command_line
47 if darcsversion.respond_to?(:force_encoding)
48 darcsversion.force_encoding('ASCII-8BIT')
49 end
47 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
50 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
48 m[2].scan(%r{\d+}).collect(&:to_i)
51 m[2].scan(%r{\d+}).collect(&:to_i)
49 end
52 end
50 end
53 end
51
54
52 def darcs_binary_version_from_command_line
55 def darcs_binary_version_from_command_line
53 shellout("#{sq_bin} --version") { |io| io.read }.to_s
56 shellout("#{sq_bin} --version") { |io| io.read }.to_s
54 end
57 end
55 end
58 end
56
59
57 def initialize(url, root_url=nil, login=nil, password=nil)
60 def initialize(url, root_url=nil, login=nil, password=nil)
58 @url = url
61 @url = url
59 @root_url = url
62 @root_url = url
60 end
63 end
61
64
62 def supports_cat?
65 def supports_cat?
63 # cat supported in darcs 2.0.0 and higher
66 # cat supported in darcs 2.0.0 and higher
64 self.class.client_version_above?([2, 0, 0])
67 self.class.client_version_above?([2, 0, 0])
65 end
68 end
66
69
67 # Get info about the darcs repository
70 # Get info about the darcs repository
68 def info
71 def info
69 rev = revisions(nil,nil,nil,{:limit => 1})
72 rev = revisions(nil,nil,nil,{:limit => 1})
70 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
73 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
71 end
74 end
72
75
73 # Returns an Entries collection
76 # Returns an Entries collection
74 # or nil if the given path doesn't exist in the repository
77 # or nil if the given path doesn't exist in the repository
75 def entries(path=nil, identifier=nil)
78 def entries(path=nil, identifier=nil)
76 path_prefix = (path.blank? ? '' : "#{path}/")
79 path_prefix = (path.blank? ? '' : "#{path}/")
77 if path.blank?
80 if path.blank?
78 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
81 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
79 end
82 end
80 entries = Entries.new
83 entries = Entries.new
81 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
84 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
82 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
85 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
83 cmd << " #{shell_quote path}"
86 cmd << " #{shell_quote path}"
84 shellout(cmd) do |io|
87 shellout(cmd) do |io|
85 begin
88 begin
86 doc = REXML::Document.new(io)
89 doc = REXML::Document.new(io)
87 if doc.root.name == 'directory'
90 if doc.root.name == 'directory'
88 doc.elements.each('directory/*') do |element|
91 doc.elements.each('directory/*') do |element|
89 next unless ['file', 'directory'].include? element.name
92 next unless ['file', 'directory'].include? element.name
90 entries << entry_from_xml(element, path_prefix)
93 entries << entry_from_xml(element, path_prefix)
91 end
94 end
92 elsif doc.root.name == 'file'
95 elsif doc.root.name == 'file'
93 entries << entry_from_xml(doc.root, path_prefix)
96 entries << entry_from_xml(doc.root, path_prefix)
94 end
97 end
95 rescue
98 rescue
96 end
99 end
97 end
100 end
98 return nil if $? && $?.exitstatus != 0
101 return nil if $? && $?.exitstatus != 0
99 entries.compact.sort_by_name
102 entries.compact.sort_by_name
100 end
103 end
101
104
102 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
105 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
103 path = '.' if path.blank?
106 path = '.' if path.blank?
104 revisions = Revisions.new
107 revisions = Revisions.new
105 cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
108 cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
106 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
109 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
107 cmd << " --last #{options[:limit].to_i}" if options[:limit]
110 cmd << " --last #{options[:limit].to_i}" if options[:limit]
108 shellout(cmd) do |io|
111 shellout(cmd) do |io|
109 begin
112 begin
110 doc = REXML::Document.new(io)
113 doc = REXML::Document.new(io)
111 doc.elements.each("changelog/patch") do |patch|
114 doc.elements.each("changelog/patch") do |patch|
112 message = patch.elements['name'].text
115 message = patch.elements['name'].text
113 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
116 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
114 revisions << Revision.new({:identifier => nil,
117 revisions << Revision.new({:identifier => nil,
115 :author => patch.attributes['author'],
118 :author => patch.attributes['author'],
116 :scmid => patch.attributes['hash'],
119 :scmid => patch.attributes['hash'],
117 :time => Time.parse(patch.attributes['local_date']),
120 :time => Time.parse(patch.attributes['local_date']),
118 :message => message,
121 :message => message,
119 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
122 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
120 })
123 })
121 end
124 end
122 rescue
125 rescue
123 end
126 end
124 end
127 end
125 return nil if $? && $?.exitstatus != 0
128 return nil if $? && $?.exitstatus != 0
126 revisions
129 revisions
127 end
130 end
128
131
129 def diff(path, identifier_from, identifier_to=nil)
132 def diff(path, identifier_from, identifier_to=nil)
130 path = '*' if path.blank?
133 path = '*' if path.blank?
131 cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
134 cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
132 if identifier_to.nil?
135 if identifier_to.nil?
133 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
136 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
134 else
137 else
135 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
138 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
136 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
139 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
137 end
140 end
138 cmd << " -u #{shell_quote path}"
141 cmd << " -u #{shell_quote path}"
139 diff = []
142 diff = []
140 shellout(cmd) do |io|
143 shellout(cmd) do |io|
141 io.each_line do |line|
144 io.each_line do |line|
142 diff << line
145 diff << line
143 end
146 end
144 end
147 end
145 return nil if $? && $?.exitstatus != 0
148 return nil if $? && $?.exitstatus != 0
146 diff
149 diff
147 end
150 end
148
151
149 def cat(path, identifier=nil)
152 def cat(path, identifier=nil)
150 cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
153 cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
151 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
154 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
152 cmd << " #{shell_quote path}"
155 cmd << " #{shell_quote path}"
153 cat = nil
156 cat = nil
154 shellout(cmd) do |io|
157 shellout(cmd) do |io|
155 io.binmode
158 io.binmode
156 cat = io.read
159 cat = io.read
157 end
160 end
158 return nil if $? && $?.exitstatus != 0
161 return nil if $? && $?.exitstatus != 0
159 cat
162 cat
160 end
163 end
161
164
162 private
165 private
163
166
164 # Returns an Entry from the given XML element
167 # Returns an Entry from the given XML element
165 # or nil if the entry was deleted
168 # or nil if the entry was deleted
166 def entry_from_xml(element, path_prefix)
169 def entry_from_xml(element, path_prefix)
167 modified_element = element.elements['modified']
170 modified_element = element.elements['modified']
168 if modified_element.elements['modified_how'].text.match(/removed/)
171 if modified_element.elements['modified_how'].text.match(/removed/)
169 return nil
172 return nil
170 end
173 end
171
174
172 Entry.new({:name => element.attributes['name'],
175 Entry.new({:name => element.attributes['name'],
173 :path => path_prefix + element.attributes['name'],
176 :path => path_prefix + element.attributes['name'],
174 :kind => element.name == 'file' ? 'file' : 'dir',
177 :kind => element.name == 'file' ? 'file' : 'dir',
175 :size => nil,
178 :size => nil,
176 :lastrev => Revision.new({
179 :lastrev => Revision.new({
177 :identifier => nil,
180 :identifier => nil,
178 :scmid => modified_element.elements['patch'].attributes['hash']
181 :scmid => modified_element.elements['patch'].attributes['hash']
179 })
182 })
180 })
183 })
181 end
184 end
182
185
183 def get_paths_for_patch(hash)
186 def get_paths_for_patch(hash)
184 paths = get_paths_for_patch_raw(hash)
187 paths = get_paths_for_patch_raw(hash)
185 if self.class.client_version_above?([2, 4])
188 if self.class.client_version_above?([2, 4])
186 orig_paths = paths
189 orig_paths = paths
187 paths = []
190 paths = []
188 add_paths = []
191 add_paths = []
189 add_paths_name = []
192 add_paths_name = []
190 mod_paths = []
193 mod_paths = []
191 other_paths = []
194 other_paths = []
192 orig_paths.each do |path|
195 orig_paths.each do |path|
193 if path[:action] == 'A'
196 if path[:action] == 'A'
194 add_paths << path
197 add_paths << path
195 add_paths_name << path[:path]
198 add_paths_name << path[:path]
196 elsif path[:action] == 'M'
199 elsif path[:action] == 'M'
197 mod_paths << path
200 mod_paths << path
198 else
201 else
199 other_paths << path
202 other_paths << path
200 end
203 end
201 end
204 end
202 add_paths_name.each do |add_path|
205 add_paths_name.each do |add_path|
203 mod_paths.delete_if { |m| m[:path] == add_path }
206 mod_paths.delete_if { |m| m[:path] == add_path }
204 end
207 end
205 paths.concat add_paths
208 paths.concat add_paths
206 paths.concat mod_paths
209 paths.concat mod_paths
207 paths.concat other_paths
210 paths.concat other_paths
208 end
211 end
209 paths
212 paths
210 end
213 end
211
214
212 # Retrieve changed paths for a single patch
215 # Retrieve changed paths for a single patch
213 def get_paths_for_patch_raw(hash)
216 def get_paths_for_patch_raw(hash)
214 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
217 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
215 cmd << " --match #{shell_quote("hash #{hash}")} "
218 cmd << " --match #{shell_quote("hash #{hash}")} "
216 paths = []
219 paths = []
217 shellout(cmd) do |io|
220 shellout(cmd) do |io|
218 begin
221 begin
219 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
222 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
220 # A root element is added so that REXML doesn't raise an error
223 # A root element is added so that REXML doesn't raise an error
221 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
224 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
222 doc.elements.each('fake_root/summary/*') do |modif|
225 doc.elements.each('fake_root/summary/*') do |modif|
223 paths << {:action => modif.name[0,1].upcase,
226 paths << {:action => modif.name[0,1].upcase,
224 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
227 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
225 }
228 }
226 end
229 end
227 rescue
230 rescue
228 end
231 end
229 end
232 end
230 paths
233 paths
231 rescue CommandFailed
234 rescue CommandFailed
232 paths
235 paths
233 end
236 end
234 end
237 end
235 end
238 end
236 end
239 end
237 end
240 end
@@ -1,322 +1,325
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class GitAdapter < AbstractAdapter
23 class GitAdapter < AbstractAdapter
24 # Git executable name
24 # Git executable name
25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
26
26
27 # raised if scm command exited with error, e.g. unknown revision.
27 # raised if scm command exited with error, e.g. unknown revision.
28 class ScmCommandAborted < CommandFailed; end
28 class ScmCommandAborted < CommandFailed; end
29
29
30 class << self
30 class << self
31 def client_command
31 def client_command
32 @@bin ||= GIT_BIN
32 @@bin ||= GIT_BIN
33 end
33 end
34
34
35 def sq_bin
35 def sq_bin
36 @@sq_bin ||= shell_quote(GIT_BIN)
36 @@sq_bin ||= shell_quote(GIT_BIN)
37 end
37 end
38
38
39 def client_version
39 def client_version
40 @@client_version ||= (scm_command_version || [])
40 @@client_version ||= (scm_command_version || [])
41 end
41 end
42
42
43 def client_available
43 def client_available
44 !client_version.empty?
44 !client_version.empty?
45 end
45 end
46
46
47 def scm_command_version
47 def scm_command_version
48 scm_version = scm_version_from_command_line
48 scm_version = scm_version_from_command_line
49 if scm_version.respond_to?(:force_encoding)
50 scm_version.force_encoding('ASCII-8BIT')
51 end
49 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
52 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
50 m[2].scan(%r{\d+}).collect(&:to_i)
53 m[2].scan(%r{\d+}).collect(&:to_i)
51 end
54 end
52 end
55 end
53
56
54 def scm_version_from_command_line
57 def scm_version_from_command_line
55 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
58 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
56 end
59 end
57 end
60 end
58
61
59 def info
62 def info
60 begin
63 begin
61 Info.new(:root_url => url, :lastrev => lastrev('',nil))
64 Info.new(:root_url => url, :lastrev => lastrev('',nil))
62 rescue
65 rescue
63 nil
66 nil
64 end
67 end
65 end
68 end
66
69
67 def branches
70 def branches
68 return @branches if @branches
71 return @branches if @branches
69 @branches = []
72 @branches = []
70 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
73 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
71 shellout(cmd) do |io|
74 shellout(cmd) do |io|
72 io.each_line do |line|
75 io.each_line do |line|
73 @branches << line.match('\s*\*?\s*(.*)$')[1]
76 @branches << line.match('\s*\*?\s*(.*)$')[1]
74 end
77 end
75 end
78 end
76 @branches.sort!
79 @branches.sort!
77 end
80 end
78
81
79 def tags
82 def tags
80 return @tags if @tags
83 return @tags if @tags
81 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
84 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
82 shellout(cmd) do |io|
85 shellout(cmd) do |io|
83 @tags = io.readlines.sort!.map{|t| t.strip}
86 @tags = io.readlines.sort!.map{|t| t.strip}
84 end
87 end
85 end
88 end
86
89
87 def default_branch
90 def default_branch
88 branches.include?('master') ? 'master' : branches.first
91 branches.include?('master') ? 'master' : branches.first
89 end
92 end
90
93
91 def entries(path=nil, identifier=nil)
94 def entries(path=nil, identifier=nil)
92 path ||= ''
95 path ||= ''
93 entries = Entries.new
96 entries = Entries.new
94 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
97 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
95 cmd << shell_quote("HEAD:" + path) if identifier.nil?
98 cmd << shell_quote("HEAD:" + path) if identifier.nil?
96 cmd << shell_quote(identifier + ":" + path) if identifier
99 cmd << shell_quote(identifier + ":" + path) if identifier
97 shellout(cmd) do |io|
100 shellout(cmd) do |io|
98 io.each_line do |line|
101 io.each_line do |line|
99 e = line.chomp.to_s
102 e = line.chomp.to_s
100 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
103 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
101 type = $1
104 type = $1
102 sha = $2
105 sha = $2
103 size = $3
106 size = $3
104 name = $4
107 name = $4
105 full_path = path.empty? ? name : "#{path}/#{name}"
108 full_path = path.empty? ? name : "#{path}/#{name}"
106 entries << Entry.new({:name => name,
109 entries << Entry.new({:name => name,
107 :path => full_path,
110 :path => full_path,
108 :kind => (type == "tree") ? 'dir' : 'file',
111 :kind => (type == "tree") ? 'dir' : 'file',
109 :size => (type == "tree") ? nil : size,
112 :size => (type == "tree") ? nil : size,
110 :lastrev => lastrev(full_path,identifier)
113 :lastrev => lastrev(full_path,identifier)
111 }) unless entries.detect{|entry| entry.name == name}
114 }) unless entries.detect{|entry| entry.name == name}
112 end
115 end
113 end
116 end
114 end
117 end
115 return nil if $? && $?.exitstatus != 0
118 return nil if $? && $?.exitstatus != 0
116 entries.sort_by_name
119 entries.sort_by_name
117 end
120 end
118
121
119 def lastrev(path,rev)
122 def lastrev(path,rev)
120 return nil if path.nil?
123 return nil if path.nil?
121 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
124 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
122 cmd << " #{shell_quote rev} " if rev
125 cmd << " #{shell_quote rev} " if rev
123 cmd << "-- #{shell_quote path} " unless path.empty?
126 cmd << "-- #{shell_quote path} " unless path.empty?
124 lines = []
127 lines = []
125 shellout(cmd) { |io| lines = io.readlines }
128 shellout(cmd) { |io| lines = io.readlines }
126 return nil if $? && $?.exitstatus != 0
129 return nil if $? && $?.exitstatus != 0
127 begin
130 begin
128 id = lines[0].split[1]
131 id = lines[0].split[1]
129 author = lines[1].match('Author:\s+(.*)$')[1]
132 author = lines[1].match('Author:\s+(.*)$')[1]
130 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
133 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
131
134
132 Revision.new({
135 Revision.new({
133 :identifier => id,
136 :identifier => id,
134 :scmid => id,
137 :scmid => id,
135 :author => author,
138 :author => author,
136 :time => time,
139 :time => time,
137 :message => nil,
140 :message => nil,
138 :paths => nil
141 :paths => nil
139 })
142 })
140 rescue NoMethodError => e
143 rescue NoMethodError => e
141 logger.error("The revision '#{path}' has a wrong format")
144 logger.error("The revision '#{path}' has a wrong format")
142 return nil
145 return nil
143 end
146 end
144 end
147 end
145
148
146 def revisions(path, identifier_from, identifier_to, options={})
149 def revisions(path, identifier_from, identifier_to, options={})
147 revisions = Revisions.new
150 revisions = Revisions.new
148 cmd_args = %w|log --no-color --raw --date=iso --pretty=fuller|
151 cmd_args = %w|log --no-color --raw --date=iso --pretty=fuller|
149 cmd_args << "--reverse" if options[:reverse]
152 cmd_args << "--reverse" if options[:reverse]
150 cmd_args << "--all" if options[:all]
153 cmd_args << "--all" if options[:all]
151 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
154 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
152 from_to = ""
155 from_to = ""
153 from_to << "#{identifier_from}.." if identifier_from
156 from_to << "#{identifier_from}.." if identifier_from
154 from_to << "#{identifier_to}" if identifier_to
157 from_to << "#{identifier_to}" if identifier_to
155 cmd_args << from_to if !from_to.empty?
158 cmd_args << from_to if !from_to.empty?
156 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
159 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
157 cmd_args << "--" << "#{path}" if path && !path.empty?
160 cmd_args << "--" << "#{path}" if path && !path.empty?
158
161
159 scm_cmd *cmd_args do |io|
162 scm_cmd *cmd_args do |io|
160 files=[]
163 files=[]
161 changeset = {}
164 changeset = {}
162 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
165 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
163 revno = 1
166 revno = 1
164
167
165 io.each_line do |line|
168 io.each_line do |line|
166 if line =~ /^commit ([0-9a-f]{40})$/
169 if line =~ /^commit ([0-9a-f]{40})$/
167 key = "commit"
170 key = "commit"
168 value = $1
171 value = $1
169 if (parsing_descr == 1 || parsing_descr == 2)
172 if (parsing_descr == 1 || parsing_descr == 2)
170 parsing_descr = 0
173 parsing_descr = 0
171 revision = Revision.new({
174 revision = Revision.new({
172 :identifier => changeset[:commit],
175 :identifier => changeset[:commit],
173 :scmid => changeset[:commit],
176 :scmid => changeset[:commit],
174 :author => changeset[:author],
177 :author => changeset[:author],
175 :time => Time.parse(changeset[:date]),
178 :time => Time.parse(changeset[:date]),
176 :message => changeset[:description],
179 :message => changeset[:description],
177 :paths => files
180 :paths => files
178 })
181 })
179 if block_given?
182 if block_given?
180 yield revision
183 yield revision
181 else
184 else
182 revisions << revision
185 revisions << revision
183 end
186 end
184 changeset = {}
187 changeset = {}
185 files = []
188 files = []
186 revno = revno + 1
189 revno = revno + 1
187 end
190 end
188 changeset[:commit] = $1
191 changeset[:commit] = $1
189 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
192 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
190 key = $1
193 key = $1
191 value = $2
194 value = $2
192 if key == "Author"
195 if key == "Author"
193 changeset[:author] = value
196 changeset[:author] = value
194 elsif key == "CommitDate"
197 elsif key == "CommitDate"
195 changeset[:date] = value
198 changeset[:date] = value
196 end
199 end
197 elsif (parsing_descr == 0) && line.chomp.to_s == ""
200 elsif (parsing_descr == 0) && line.chomp.to_s == ""
198 parsing_descr = 1
201 parsing_descr = 1
199 changeset[:description] = ""
202 changeset[:description] = ""
200 elsif (parsing_descr == 1 || parsing_descr == 2) \
203 elsif (parsing_descr == 1 || parsing_descr == 2) \
201 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
204 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
202 parsing_descr = 2
205 parsing_descr = 2
203 fileaction = $1
206 fileaction = $1
204 filepath = $2
207 filepath = $2
205 files << {:action => fileaction, :path => filepath}
208 files << {:action => fileaction, :path => filepath}
206 elsif (parsing_descr == 1 || parsing_descr == 2) \
209 elsif (parsing_descr == 1 || parsing_descr == 2) \
207 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
210 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
208 parsing_descr = 2
211 parsing_descr = 2
209 fileaction = $1
212 fileaction = $1
210 filepath = $3
213 filepath = $3
211 files << {:action => fileaction, :path => filepath}
214 files << {:action => fileaction, :path => filepath}
212 elsif (parsing_descr == 1) && line.chomp.to_s == ""
215 elsif (parsing_descr == 1) && line.chomp.to_s == ""
213 parsing_descr = 2
216 parsing_descr = 2
214 elsif (parsing_descr == 1)
217 elsif (parsing_descr == 1)
215 changeset[:description] << line[4..-1]
218 changeset[:description] << line[4..-1]
216 end
219 end
217 end
220 end
218
221
219 if changeset[:commit]
222 if changeset[:commit]
220 revision = Revision.new({
223 revision = Revision.new({
221 :identifier => changeset[:commit],
224 :identifier => changeset[:commit],
222 :scmid => changeset[:commit],
225 :scmid => changeset[:commit],
223 :author => changeset[:author],
226 :author => changeset[:author],
224 :time => Time.parse(changeset[:date]),
227 :time => Time.parse(changeset[:date]),
225 :message => changeset[:description],
228 :message => changeset[:description],
226 :paths => files
229 :paths => files
227 })
230 })
228
231
229 if block_given?
232 if block_given?
230 yield revision
233 yield revision
231 else
234 else
232 revisions << revision
235 revisions << revision
233 end
236 end
234 end
237 end
235 end
238 end
236 revisions
239 revisions
237 rescue ScmCommandAborted
240 rescue ScmCommandAborted
238 revisions
241 revisions
239 end
242 end
240
243
241 def diff(path, identifier_from, identifier_to=nil)
244 def diff(path, identifier_from, identifier_to=nil)
242 path ||= ''
245 path ||= ''
243
246
244 if identifier_to
247 if identifier_to
245 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
248 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
246 else
249 else
247 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
250 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
248 end
251 end
249
252
250 cmd << " -- #{shell_quote path}" unless path.empty?
253 cmd << " -- #{shell_quote path}" unless path.empty?
251 diff = []
254 diff = []
252 shellout(cmd) do |io|
255 shellout(cmd) do |io|
253 io.each_line do |line|
256 io.each_line do |line|
254 diff << line
257 diff << line
255 end
258 end
256 end
259 end
257 return nil if $? && $?.exitstatus != 0
260 return nil if $? && $?.exitstatus != 0
258 diff
261 diff
259 end
262 end
260
263
261 def annotate(path, identifier=nil)
264 def annotate(path, identifier=nil)
262 identifier = 'HEAD' if identifier.blank?
265 identifier = 'HEAD' if identifier.blank?
263 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
266 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
264 blame = Annotate.new
267 blame = Annotate.new
265 content = nil
268 content = nil
266 shellout(cmd) { |io| io.binmode; content = io.read }
269 shellout(cmd) { |io| io.binmode; content = io.read }
267 return nil if $? && $?.exitstatus != 0
270 return nil if $? && $?.exitstatus != 0
268 # git annotates binary files
271 # git annotates binary files
269 return nil if content.is_binary_data?
272 return nil if content.is_binary_data?
270 identifier = ''
273 identifier = ''
271 # git shows commit author on the first occurrence only
274 # git shows commit author on the first occurrence only
272 authors_by_commit = {}
275 authors_by_commit = {}
273 content.split("\n").each do |line|
276 content.split("\n").each do |line|
274 if line =~ /^([0-9a-f]{39,40})\s.*/
277 if line =~ /^([0-9a-f]{39,40})\s.*/
275 identifier = $1
278 identifier = $1
276 elsif line =~ /^author (.+)/
279 elsif line =~ /^author (.+)/
277 authors_by_commit[identifier] = $1.strip
280 authors_by_commit[identifier] = $1.strip
278 elsif line =~ /^\t(.*)/
281 elsif line =~ /^\t(.*)/
279 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
282 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
280 identifier = ''
283 identifier = ''
281 author = ''
284 author = ''
282 end
285 end
283 end
286 end
284 blame
287 blame
285 end
288 end
286
289
287 def cat(path, identifier=nil)
290 def cat(path, identifier=nil)
288 if identifier.nil?
291 if identifier.nil?
289 identifier = 'HEAD'
292 identifier = 'HEAD'
290 end
293 end
291 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
294 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
292 cat = nil
295 cat = nil
293 shellout(cmd) do |io|
296 shellout(cmd) do |io|
294 io.binmode
297 io.binmode
295 cat = io.read
298 cat = io.read
296 end
299 end
297 return nil if $? && $?.exitstatus != 0
300 return nil if $? && $?.exitstatus != 0
298 cat
301 cat
299 end
302 end
300
303
301 class Revision < Redmine::Scm::Adapters::Revision
304 class Revision < Redmine::Scm::Adapters::Revision
302 # Returns the readable identifier
305 # Returns the readable identifier
303 def format_identifier
306 def format_identifier
304 identifier[0,8]
307 identifier[0,8]
305 end
308 end
306 end
309 end
307
310
308 def scm_cmd(*args, &block)
311 def scm_cmd(*args, &block)
309 repo_path = root_url || url
312 repo_path = root_url || url
310 full_args = [GIT_BIN, '--git-dir', repo_path]
313 full_args = [GIT_BIN, '--git-dir', repo_path]
311 full_args += args
314 full_args += args
312 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
315 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
313 if $? && $?.exitstatus != 0
316 if $? && $?.exitstatus != 0
314 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
317 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
315 end
318 end
316 ret
319 ret
317 end
320 end
318 private :scm_cmd
321 private :scm_cmd
319 end
322 end
320 end
323 end
321 end
324 end
322 end
325 end
@@ -1,291 +1,294
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'cgi'
19 require 'cgi'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class MercurialAdapter < AbstractAdapter
24 class MercurialAdapter < AbstractAdapter
25
25
26 # Mercurial executable name
26 # Mercurial executable name
27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
28 HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
29 HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
29 HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
30 TEMPLATE_NAME = "hg-template"
30 TEMPLATE_NAME = "hg-template"
31 TEMPLATE_EXTENSION = "tmpl"
31 TEMPLATE_EXTENSION = "tmpl"
32
32
33 # raised if hg command exited with error, e.g. unknown revision.
33 # raised if hg command exited with error, e.g. unknown revision.
34 class HgCommandAborted < CommandFailed; end
34 class HgCommandAborted < CommandFailed; end
35
35
36 class << self
36 class << self
37 def client_command
37 def client_command
38 @@bin ||= HG_BIN
38 @@bin ||= HG_BIN
39 end
39 end
40
40
41 def sq_bin
41 def sq_bin
42 @@sq_bin ||= shell_quote(HG_BIN)
42 @@sq_bin ||= shell_quote(HG_BIN)
43 end
43 end
44
44
45 def client_version
45 def client_version
46 @@client_version ||= (hgversion || [])
46 @@client_version ||= (hgversion || [])
47 end
47 end
48
48
49 def client_available
49 def client_available
50 !client_version.empty?
50 !client_version.empty?
51 end
51 end
52
52
53 def hgversion
53 def hgversion
54 # The hg version is expressed either as a
54 # The hg version is expressed either as a
55 # release number (eg 0.9.5 or 1.0) or as a revision
55 # release number (eg 0.9.5 or 1.0) or as a revision
56 # id composed of 12 hexa characters.
56 # id composed of 12 hexa characters.
57 theversion = hgversion_from_command_line
57 theversion = hgversion_from_command_line
58 if theversion.respond_to?(:force_encoding)
59 theversion.force_encoding('ASCII-8BIT')
60 end
58 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
61 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
59 m[2].scan(%r{\d+}).collect(&:to_i)
62 m[2].scan(%r{\d+}).collect(&:to_i)
60 end
63 end
61 end
64 end
62
65
63 def hgversion_from_command_line
66 def hgversion_from_command_line
64 shellout("#{sq_bin} --version") { |io| io.read }.to_s
67 shellout("#{sq_bin} --version") { |io| io.read }.to_s
65 end
68 end
66
69
67 def template_path
70 def template_path
68 @@template_path ||= template_path_for(client_version)
71 @@template_path ||= template_path_for(client_version)
69 end
72 end
70
73
71 def template_path_for(version)
74 def template_path_for(version)
72 if ((version <=> [0,9,5]) > 0) || version.empty?
75 if ((version <=> [0,9,5]) > 0) || version.empty?
73 ver = "1.0"
76 ver = "1.0"
74 else
77 else
75 ver = "0.9.5"
78 ver = "0.9.5"
76 end
79 end
77 "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
80 "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
78 end
81 end
79 end
82 end
80
83
81 def initialize(url, root_url=nil, login=nil, password=nil)
84 def initialize(url, root_url=nil, login=nil, password=nil)
82 super
85 super
83 @path_encoding = 'UTF-8'
86 @path_encoding = 'UTF-8'
84 end
87 end
85
88
86 def info
89 def info
87 tip = summary['repository']['tip']
90 tip = summary['repository']['tip']
88 Info.new(:root_url => CGI.unescape(summary['repository']['root']),
91 Info.new(:root_url => CGI.unescape(summary['repository']['root']),
89 :lastrev => Revision.new(:revision => tip['revision'],
92 :lastrev => Revision.new(:revision => tip['revision'],
90 :scmid => tip['node']))
93 :scmid => tip['node']))
91 end
94 end
92
95
93 def tags
96 def tags
94 as_ary(summary['repository']['tag']).map { |e| e['name'] }
97 as_ary(summary['repository']['tag']).map { |e| e['name'] }
95 end
98 end
96
99
97 # Returns map of {'tag' => 'nodeid', ...}
100 # Returns map of {'tag' => 'nodeid', ...}
98 def tagmap
101 def tagmap
99 alist = as_ary(summary['repository']['tag']).map do |e|
102 alist = as_ary(summary['repository']['tag']).map do |e|
100 e.values_at('name', 'node')
103 e.values_at('name', 'node')
101 end
104 end
102 Hash[*alist.flatten]
105 Hash[*alist.flatten]
103 end
106 end
104
107
105 def branches
108 def branches
106 as_ary(summary['repository']['branch']).map { |e| e['name'] }
109 as_ary(summary['repository']['branch']).map { |e| e['name'] }
107 end
110 end
108
111
109 # Returns map of {'branch' => 'nodeid', ...}
112 # Returns map of {'branch' => 'nodeid', ...}
110 def branchmap
113 def branchmap
111 alist = as_ary(summary['repository']['branch']).map do |e|
114 alist = as_ary(summary['repository']['branch']).map do |e|
112 e.values_at('name', 'node')
115 e.values_at('name', 'node')
113 end
116 end
114 Hash[*alist.flatten]
117 Hash[*alist.flatten]
115 end
118 end
116
119
117 def summary
120 def summary
118 return @summary if @summary
121 return @summary if @summary
119 hg 'rhsummary' do |io|
122 hg 'rhsummary' do |io|
120 begin
123 begin
121 @summary = ActiveSupport::XmlMini.parse(io.read)['rhsummary']
124 @summary = ActiveSupport::XmlMini.parse(io.read)['rhsummary']
122 rescue
125 rescue
123 end
126 end
124 end
127 end
125 end
128 end
126 private :summary
129 private :summary
127
130
128 def entries(path=nil, identifier=nil)
131 def entries(path=nil, identifier=nil)
129 manifest = hg('rhmanifest', '-r', hgrev(identifier),
132 manifest = hg('rhmanifest', '-r', hgrev(identifier),
130 CGI.escape(without_leading_slash(path.to_s))) do |io|
133 CGI.escape(without_leading_slash(path.to_s))) do |io|
131 begin
134 begin
132 ActiveSupport::XmlMini.parse(io.read)['rhmanifest']['repository']['manifest']
135 ActiveSupport::XmlMini.parse(io.read)['rhmanifest']['repository']['manifest']
133 rescue
136 rescue
134 end
137 end
135 end
138 end
136 path_prefix = path.blank? ? '' : with_trailling_slash(path)
139 path_prefix = path.blank? ? '' : with_trailling_slash(path)
137
140
138 entries = Entries.new
141 entries = Entries.new
139 as_ary(manifest['dir']).each do |e|
142 as_ary(manifest['dir']).each do |e|
140 n = CGI.unescape(e['name'])
143 n = CGI.unescape(e['name'])
141 p = "#{path_prefix}#{n}"
144 p = "#{path_prefix}#{n}"
142 entries << Entry.new(:name => n, :path => p, :kind => 'dir')
145 entries << Entry.new(:name => n, :path => p, :kind => 'dir')
143 end
146 end
144
147
145 as_ary(manifest['file']).each do |e|
148 as_ary(manifest['file']).each do |e|
146 n = CGI.unescape(e['name'])
149 n = CGI.unescape(e['name'])
147 p = "#{path_prefix}#{n}"
150 p = "#{path_prefix}#{n}"
148 lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
151 lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
149 :identifier => e['node'],
152 :identifier => e['node'],
150 :time => Time.at(e['time'].to_i))
153 :time => Time.at(e['time'].to_i))
151 entries << Entry.new(:name => n, :path => p, :kind => 'file',
154 entries << Entry.new(:name => n, :path => p, :kind => 'file',
152 :size => e['size'].to_i, :lastrev => lr)
155 :size => e['size'].to_i, :lastrev => lr)
153 end
156 end
154
157
155 entries
158 entries
156 rescue HgCommandAborted
159 rescue HgCommandAborted
157 nil # means not found
160 nil # means not found
158 end
161 end
159
162
160 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
163 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
161 revs = Revisions.new
164 revs = Revisions.new
162 each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
165 each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
163 revs
166 revs
164 end
167 end
165
168
166 # Iterates the revisions by using a template file that
169 # Iterates the revisions by using a template file that
167 # makes Mercurial produce a xml output.
170 # makes Mercurial produce a xml output.
168 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
171 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
169 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
172 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
170 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
173 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
171 hg_args << '--limit' << options[:limit] if options[:limit]
174 hg_args << '--limit' << options[:limit] if options[:limit]
172 hg_args << hgtarget(path) unless path.blank?
175 hg_args << hgtarget(path) unless path.blank?
173 log = hg(*hg_args) do |io|
176 log = hg(*hg_args) do |io|
174 begin
177 begin
175 # Mercurial < 1.5 does not support footer template for '</log>'
178 # Mercurial < 1.5 does not support footer template for '</log>'
176 ActiveSupport::XmlMini.parse("#{io.read}</log>")['log']
179 ActiveSupport::XmlMini.parse("#{io.read}</log>")['log']
177 rescue
180 rescue
178 end
181 end
179 end
182 end
180
183
181 as_ary(log['logentry']).each do |le|
184 as_ary(log['logentry']).each do |le|
182 cpalist = as_ary(le['paths']['path-copied']).map do |e|
185 cpalist = as_ary(le['paths']['path-copied']).map do |e|
183 [e['__content__'], e['copyfrom-path']].map { |s| CGI.unescape(s) }
186 [e['__content__'], e['copyfrom-path']].map { |s| CGI.unescape(s) }
184 end
187 end
185 cpmap = Hash[*cpalist.flatten]
188 cpmap = Hash[*cpalist.flatten]
186
189
187 paths = as_ary(le['paths']['path']).map do |e|
190 paths = as_ary(le['paths']['path']).map do |e|
188 p = CGI.unescape(e['__content__'])
191 p = CGI.unescape(e['__content__'])
189 {:action => e['action'], :path => with_leading_slash(p),
192 {:action => e['action'], :path => with_leading_slash(p),
190 :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
193 :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
191 :from_revision => (cpmap.member?(p) ? le['revision'] : nil)}
194 :from_revision => (cpmap.member?(p) ? le['revision'] : nil)}
192 end.sort { |a, b| a[:path] <=> b[:path] }
195 end.sort { |a, b| a[:path] <=> b[:path] }
193
196
194 yield Revision.new(:revision => le['revision'],
197 yield Revision.new(:revision => le['revision'],
195 :scmid => le['node'],
198 :scmid => le['node'],
196 :author => (le['author']['__content__'] rescue ''),
199 :author => (le['author']['__content__'] rescue ''),
197 :time => Time.parse(le['date']['__content__']).localtime,
200 :time => Time.parse(le['date']['__content__']).localtime,
198 :message => le['msg']['__content__'],
201 :message => le['msg']['__content__'],
199 :paths => paths)
202 :paths => paths)
200 end
203 end
201 self
204 self
202 end
205 end
203
206
204 def diff(path, identifier_from, identifier_to=nil)
207 def diff(path, identifier_from, identifier_to=nil)
205 hg_args = %w|rhdiff|
208 hg_args = %w|rhdiff|
206 if identifier_to
209 if identifier_to
207 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
210 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
208 else
211 else
209 hg_args << '-c' << hgrev(identifier_from)
212 hg_args << '-c' << hgrev(identifier_from)
210 end
213 end
211 hg_args << CGI.escape(hgtarget(path)) unless path.blank?
214 hg_args << CGI.escape(hgtarget(path)) unless path.blank?
212 diff = []
215 diff = []
213 hg *hg_args do |io|
216 hg *hg_args do |io|
214 io.each_line do |line|
217 io.each_line do |line|
215 diff << line
218 diff << line
216 end
219 end
217 end
220 end
218 diff
221 diff
219 rescue HgCommandAborted
222 rescue HgCommandAborted
220 nil # means not found
223 nil # means not found
221 end
224 end
222
225
223 def cat(path, identifier=nil)
226 def cat(path, identifier=nil)
224 hg 'cat', '-r', hgrev(identifier), hgtarget(path) do |io|
227 hg 'cat', '-r', hgrev(identifier), hgtarget(path) do |io|
225 io.binmode
228 io.binmode
226 io.read
229 io.read
227 end
230 end
228 rescue HgCommandAborted
231 rescue HgCommandAborted
229 nil # means not found
232 nil # means not found
230 end
233 end
231
234
232 def annotate(path, identifier=nil)
235 def annotate(path, identifier=nil)
233 blame = Annotate.new
236 blame = Annotate.new
234 hg 'annotate', '-ncu', '-r', hgrev(identifier), hgtarget(path) do |io|
237 hg 'annotate', '-ncu', '-r', hgrev(identifier), hgtarget(path) do |io|
235 io.each_line do |line|
238 io.each_line do |line|
236 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
239 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
237 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
240 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
238 :identifier => $3)
241 :identifier => $3)
239 blame.add_line($4.rstrip, r)
242 blame.add_line($4.rstrip, r)
240 end
243 end
241 end
244 end
242 blame
245 blame
243 rescue HgCommandAborted
246 rescue HgCommandAborted
244 nil # means not found or cannot be annotated
247 nil # means not found or cannot be annotated
245 end
248 end
246
249
247 class Revision < Redmine::Scm::Adapters::Revision
250 class Revision < Redmine::Scm::Adapters::Revision
248 # Returns the readable identifier
251 # Returns the readable identifier
249 def format_identifier
252 def format_identifier
250 "#{revision}:#{scmid}"
253 "#{revision}:#{scmid}"
251 end
254 end
252 end
255 end
253
256
254 # Runs 'hg' command with the given args
257 # Runs 'hg' command with the given args
255 def hg(*args, &block)
258 def hg(*args, &block)
256 repo_path = root_url || url
259 repo_path = root_url || url
257 full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8']
260 full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8']
258 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
261 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
259 full_args << '--config' << 'diff.git=false'
262 full_args << '--config' << 'diff.git=false'
260 full_args += args
263 full_args += args
261 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
264 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
262 if $? && $?.exitstatus != 0
265 if $? && $?.exitstatus != 0
263 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
266 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
264 end
267 end
265 ret
268 ret
266 end
269 end
267 private :hg
270 private :hg
268
271
269 # Returns correct revision identifier
272 # Returns correct revision identifier
270 def hgrev(identifier, sq=false)
273 def hgrev(identifier, sq=false)
271 rev = identifier.blank? ? 'tip' : identifier.to_s
274 rev = identifier.blank? ? 'tip' : identifier.to_s
272 rev = shell_quote(rev) if sq
275 rev = shell_quote(rev) if sq
273 rev
276 rev
274 end
277 end
275 private :hgrev
278 private :hgrev
276
279
277 def hgtarget(path)
280 def hgtarget(path)
278 path ||= ''
281 path ||= ''
279 root_url + '/' + without_leading_slash(path)
282 root_url + '/' + without_leading_slash(path)
280 end
283 end
281 private :hgtarget
284 private :hgtarget
282
285
283 def as_ary(o)
286 def as_ary(o)
284 return [] unless o
287 return [] unless o
285 o.is_a?(Array) ? o : Array[o]
288 o.is_a?(Array) ? o : Array[o]
286 end
289 end
287 private :as_ary
290 private :as_ary
288 end
291 end
289 end
292 end
290 end
293 end
291 end
294 end
@@ -1,267 +1,270
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'uri'
19 require 'uri'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class SubversionAdapter < AbstractAdapter
24 class SubversionAdapter < AbstractAdapter
25
25
26 # SVN executable name
26 # SVN executable name
27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
28
28
29 class << self
29 class << self
30 def client_command
30 def client_command
31 @@bin ||= SVN_BIN
31 @@bin ||= SVN_BIN
32 end
32 end
33
33
34 def sq_bin
34 def sq_bin
35 @@sq_bin ||= shell_quote(SVN_BIN)
35 @@sq_bin ||= shell_quote(SVN_BIN)
36 end
36 end
37
37
38 def client_version
38 def client_version
39 @@client_version ||= (svn_binary_version || [])
39 @@client_version ||= (svn_binary_version || [])
40 end
40 end
41
41
42 def client_available
42 def client_available
43 !client_version.empty?
43 !client_version.empty?
44 end
44 end
45
45
46 def svn_binary_version
46 def svn_binary_version
47 scm_version = scm_version_from_command_line
47 scm_version = scm_version_from_command_line
48 if scm_version.respond_to?(:force_encoding)
49 scm_version.force_encoding('ASCII-8BIT')
50 end
48 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
51 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
49 m[2].scan(%r{\d+}).collect(&:to_i)
52 m[2].scan(%r{\d+}).collect(&:to_i)
50 end
53 end
51 end
54 end
52
55
53 def scm_version_from_command_line
56 def scm_version_from_command_line
54 shellout("#{sq_bin} --version") { |io| io.read }.to_s
57 shellout("#{sq_bin} --version") { |io| io.read }.to_s
55 end
58 end
56 end
59 end
57
60
58 # Get info about the svn repository
61 # Get info about the svn repository
59 def info
62 def info
60 cmd = "#{self.class.sq_bin} info --xml #{target}"
63 cmd = "#{self.class.sq_bin} info --xml #{target}"
61 cmd << credentials_string
64 cmd << credentials_string
62 info = nil
65 info = nil
63 shellout(cmd) do |io|
66 shellout(cmd) do |io|
64 output = io.read
67 output = io.read
65 begin
68 begin
66 doc = ActiveSupport::XmlMini.parse(output)
69 doc = ActiveSupport::XmlMini.parse(output)
67 #root_url = doc.elements["info/entry/repository/root"].text
70 #root_url = doc.elements["info/entry/repository/root"].text
68 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
71 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
69 :lastrev => Revision.new({
72 :lastrev => Revision.new({
70 :identifier => doc['info']['entry']['commit']['revision'],
73 :identifier => doc['info']['entry']['commit']['revision'],
71 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
74 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
72 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
75 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
73 })
76 })
74 })
77 })
75 rescue
78 rescue
76 end
79 end
77 end
80 end
78 return nil if $? && $?.exitstatus != 0
81 return nil if $? && $?.exitstatus != 0
79 info
82 info
80 rescue CommandFailed
83 rescue CommandFailed
81 return nil
84 return nil
82 end
85 end
83
86
84 # Returns an Entries collection
87 # Returns an Entries collection
85 # or nil if the given path doesn't exist in the repository
88 # or nil if the given path doesn't exist in the repository
86 def entries(path=nil, identifier=nil)
89 def entries(path=nil, identifier=nil)
87 path ||= ''
90 path ||= ''
88 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
91 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
89 entries = Entries.new
92 entries = Entries.new
90 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
93 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
91 cmd << credentials_string
94 cmd << credentials_string
92 shellout(cmd) do |io|
95 shellout(cmd) do |io|
93 output = io.read
96 output = io.read
94 begin
97 begin
95 doc = ActiveSupport::XmlMini.parse(output)
98 doc = ActiveSupport::XmlMini.parse(output)
96 each_xml_element(doc['lists']['list'], 'entry') do |entry|
99 each_xml_element(doc['lists']['list'], 'entry') do |entry|
97 commit = entry['commit']
100 commit = entry['commit']
98 commit_date = commit['date']
101 commit_date = commit['date']
99 # Skip directory if there is no commit date (usually that
102 # Skip directory if there is no commit date (usually that
100 # means that we don't have read access to it)
103 # means that we don't have read access to it)
101 next if entry['kind'] == 'dir' && commit_date.nil?
104 next if entry['kind'] == 'dir' && commit_date.nil?
102 name = entry['name']['__content__']
105 name = entry['name']['__content__']
103 entries << Entry.new({:name => URI.unescape(name),
106 entries << Entry.new({:name => URI.unescape(name),
104 :path => ((path.empty? ? "" : "#{path}/") + name),
107 :path => ((path.empty? ? "" : "#{path}/") + name),
105 :kind => entry['kind'],
108 :kind => entry['kind'],
106 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
109 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
107 :lastrev => Revision.new({
110 :lastrev => Revision.new({
108 :identifier => commit['revision'],
111 :identifier => commit['revision'],
109 :time => Time.parse(commit_date['__content__'].to_s).localtime,
112 :time => Time.parse(commit_date['__content__'].to_s).localtime,
110 :author => ((a = commit['author']) ? a['__content__'] : nil)
113 :author => ((a = commit['author']) ? a['__content__'] : nil)
111 })
114 })
112 })
115 })
113 end
116 end
114 rescue Exception => e
117 rescue Exception => e
115 logger.error("Error parsing svn output: #{e.message}")
118 logger.error("Error parsing svn output: #{e.message}")
116 logger.error("Output was:\n #{output}")
119 logger.error("Output was:\n #{output}")
117 end
120 end
118 end
121 end
119 return nil if $? && $?.exitstatus != 0
122 return nil if $? && $?.exitstatus != 0
120 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
123 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
121 entries.sort_by_name
124 entries.sort_by_name
122 end
125 end
123
126
124 def properties(path, identifier=nil)
127 def properties(path, identifier=nil)
125 # proplist xml output supported in svn 1.5.0 and higher
128 # proplist xml output supported in svn 1.5.0 and higher
126 return nil unless self.class.client_version_above?([1, 5, 0])
129 return nil unless self.class.client_version_above?([1, 5, 0])
127
130
128 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
131 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
129 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
132 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
130 cmd << credentials_string
133 cmd << credentials_string
131 properties = {}
134 properties = {}
132 shellout(cmd) do |io|
135 shellout(cmd) do |io|
133 output = io.read
136 output = io.read
134 begin
137 begin
135 doc = ActiveSupport::XmlMini.parse(output)
138 doc = ActiveSupport::XmlMini.parse(output)
136 each_xml_element(doc['properties']['target'], 'property') do |property|
139 each_xml_element(doc['properties']['target'], 'property') do |property|
137 properties[ property['name'] ] = property['__content__'].to_s
140 properties[ property['name'] ] = property['__content__'].to_s
138 end
141 end
139 rescue
142 rescue
140 end
143 end
141 end
144 end
142 return nil if $? && $?.exitstatus != 0
145 return nil if $? && $?.exitstatus != 0
143 properties
146 properties
144 end
147 end
145
148
146 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
149 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
147 path ||= ''
150 path ||= ''
148 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
151 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
149 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
152 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
150 revisions = Revisions.new
153 revisions = Revisions.new
151 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
154 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
152 cmd << credentials_string
155 cmd << credentials_string
153 cmd << " --verbose " if options[:with_paths]
156 cmd << " --verbose " if options[:with_paths]
154 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
157 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
155 cmd << ' ' + target(path)
158 cmd << ' ' + target(path)
156 shellout(cmd) do |io|
159 shellout(cmd) do |io|
157 output = io.read
160 output = io.read
158 begin
161 begin
159 doc = ActiveSupport::XmlMini.parse(output)
162 doc = ActiveSupport::XmlMini.parse(output)
160 each_xml_element(doc['log'], 'logentry') do |logentry|
163 each_xml_element(doc['log'], 'logentry') do |logentry|
161 paths = []
164 paths = []
162 each_xml_element(logentry['paths'], 'path') do |path|
165 each_xml_element(logentry['paths'], 'path') do |path|
163 paths << {:action => path['action'],
166 paths << {:action => path['action'],
164 :path => path['__content__'],
167 :path => path['__content__'],
165 :from_path => path['copyfrom-path'],
168 :from_path => path['copyfrom-path'],
166 :from_revision => path['copyfrom-rev']
169 :from_revision => path['copyfrom-rev']
167 }
170 }
168 end if logentry['paths'] && logentry['paths']['path']
171 end if logentry['paths'] && logentry['paths']['path']
169 paths.sort! { |x,y| x[:path] <=> y[:path] }
172 paths.sort! { |x,y| x[:path] <=> y[:path] }
170
173
171 revisions << Revision.new({:identifier => logentry['revision'],
174 revisions << Revision.new({:identifier => logentry['revision'],
172 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
175 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
173 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
176 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
174 :message => logentry['msg']['__content__'],
177 :message => logentry['msg']['__content__'],
175 :paths => paths
178 :paths => paths
176 })
179 })
177 end
180 end
178 rescue
181 rescue
179 end
182 end
180 end
183 end
181 return nil if $? && $?.exitstatus != 0
184 return nil if $? && $?.exitstatus != 0
182 revisions
185 revisions
183 end
186 end
184
187
185 def diff(path, identifier_from, identifier_to=nil, type="inline")
188 def diff(path, identifier_from, identifier_to=nil, type="inline")
186 path ||= ''
189 path ||= ''
187 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
190 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
188
191
189 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
192 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
190
193
191 cmd = "#{self.class.sq_bin} diff -r "
194 cmd = "#{self.class.sq_bin} diff -r "
192 cmd << "#{identifier_to}:"
195 cmd << "#{identifier_to}:"
193 cmd << "#{identifier_from}"
196 cmd << "#{identifier_from}"
194 cmd << " #{target(path)}@#{identifier_from}"
197 cmd << " #{target(path)}@#{identifier_from}"
195 cmd << credentials_string
198 cmd << credentials_string
196 diff = []
199 diff = []
197 shellout(cmd) do |io|
200 shellout(cmd) do |io|
198 io.each_line do |line|
201 io.each_line do |line|
199 diff << line
202 diff << line
200 end
203 end
201 end
204 end
202 return nil if $? && $?.exitstatus != 0
205 return nil if $? && $?.exitstatus != 0
203 diff
206 diff
204 end
207 end
205
208
206 def cat(path, identifier=nil)
209 def cat(path, identifier=nil)
207 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
208 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
211 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
209 cmd << credentials_string
212 cmd << credentials_string
210 cat = nil
213 cat = nil
211 shellout(cmd) do |io|
214 shellout(cmd) do |io|
212 io.binmode
215 io.binmode
213 cat = io.read
216 cat = io.read
214 end
217 end
215 return nil if $? && $?.exitstatus != 0
218 return nil if $? && $?.exitstatus != 0
216 cat
219 cat
217 end
220 end
218
221
219 def annotate(path, identifier=nil)
222 def annotate(path, identifier=nil)
220 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
223 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
221 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
224 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
222 cmd << credentials_string
225 cmd << credentials_string
223 blame = Annotate.new
226 blame = Annotate.new
224 shellout(cmd) do |io|
227 shellout(cmd) do |io|
225 io.each_line do |line|
228 io.each_line do |line|
226 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
229 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
227 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
230 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
228 end
231 end
229 end
232 end
230 return nil if $? && $?.exitstatus != 0
233 return nil if $? && $?.exitstatus != 0
231 blame
234 blame
232 end
235 end
233
236
234 private
237 private
235
238
236 def credentials_string
239 def credentials_string
237 str = ''
240 str = ''
238 str << " --username #{shell_quote(@login)}" unless @login.blank?
241 str << " --username #{shell_quote(@login)}" unless @login.blank?
239 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
242 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
240 str << " --no-auth-cache --non-interactive"
243 str << " --no-auth-cache --non-interactive"
241 str
244 str
242 end
245 end
243
246
244 # Helper that iterates over the child elements of a xml node
247 # Helper that iterates over the child elements of a xml node
245 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
248 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
246 def each_xml_element(node, name)
249 def each_xml_element(node, name)
247 if node && node[name]
250 if node && node[name]
248 if node[name].is_a?(Hash)
251 if node[name].is_a?(Hash)
249 yield node[name]
252 yield node[name]
250 else
253 else
251 node[name].each do |element|
254 node[name].each do |element|
252 yield element
255 yield element
253 end
256 end
254 end
257 end
255 end
258 end
256 end
259 end
257
260
258 def target(path = '')
261 def target(path = '')
259 base = path.match(/^\//) ? root_url : url
262 base = path.match(/^\//) ? root_url : url
260 uri = "#{base}/#{path}"
263 uri = "#{base}/#{path}"
261 uri = URI.escape(URI.escape(uri), '[]')
264 uri = URI.escape(URI.escape(uri), '[]')
262 shell_quote(uri.gsub(/[?<>\*]/, ''))
265 shell_quote(uri.gsub(/[?<>\*]/, ''))
263 end
266 end
264 end
267 end
265 end
268 end
266 end
269 end
267 end
270 end
General Comments 0
You need to be logged in to leave comments. Login now