Mercurial > hg > soundsoftware-site
comparison lib/redmine/scm/adapters/mercurial_adapter.rb @ 3:7c48bad7d85d yuya
* Import Mercurial overhaul patches from Yuya Nishihara (see http://www.redmine.org/issues/4455)
author | Chris Cannam |
---|---|
date | Wed, 28 Jul 2010 12:40:01 +0100 |
parents | 513646585e45 |
children | ed5f37ea0d06 |
comparison
equal
deleted
inserted
replaced
2:b940d200fbd2 | 3:7c48bad7d85d |
---|---|
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 | 20 |
20 module Redmine | 21 module Redmine |
21 module Scm | 22 module Scm |
22 module Adapters | 23 module Adapters |
23 class MercurialAdapter < AbstractAdapter | 24 class MercurialAdapter < AbstractAdapter |
24 | 25 |
25 # Mercurial executable name | 26 # Mercurial executable name |
26 HG_BIN = "hg" | 27 HG_BIN = "hg" |
28 HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py" | |
27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" | 29 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" |
28 TEMPLATE_NAME = "hg-template" | 30 TEMPLATE_NAME = "hg-template" |
29 TEMPLATE_EXTENSION = "tmpl" | 31 TEMPLATE_EXTENSION = "tmpl" |
30 | 32 |
33 # raised if hg command exited with error, e.g. unknown revision. | |
34 class HgCommandAborted < CommandFailed; end | |
35 | |
31 class << self | 36 class << self |
32 def client_version | 37 def client_version |
33 @@client_version ||= (hgversion || []) | 38 @client_version ||= hgversion |
34 end | 39 end |
35 | 40 |
36 def hgversion | 41 def hgversion |
37 # The hg version is expressed either as a | 42 # The hg version is expressed either as a |
38 # release number (eg 0.9.5 or 1.0) or as a revision | 43 # release number (eg 0.9.5 or 1.0) or as a revision |
39 # id composed of 12 hexa characters. | 44 # id composed of 12 hexa characters. |
40 theversion = hgversion_from_command_line | 45 hgversion_str.to_s.split('.').map { |e| e.to_i } |
41 if theversion.match(/^\d+(\.\d+)+/) | 46 end |
42 theversion.split(".").collect(&:to_i) | 47 private :hgversion |
43 end | 48 |
44 end | 49 def hgversion_str |
45 | 50 shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/] |
46 def hgversion_from_command_line | 51 end |
47 %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] | 52 private :hgversion_str |
48 end | |
49 | 53 |
50 def template_path | 54 def template_path |
51 @@template_path ||= template_path_for(client_version) | 55 template_path_for(client_version) |
52 end | 56 end |
53 | 57 |
54 def template_path_for(version) | 58 def template_path_for(version) |
55 if ((version <=> [0,9,5]) > 0) || version.empty? | 59 if ((version <=> [0,9,5]) > 0) || version.empty? |
56 ver = "1.0" | 60 ver = "1.0" |
57 else | 61 else |
58 ver = "0.9.5" | 62 ver = "0.9.5" |
59 end | 63 end |
60 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | 64 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" |
61 end | 65 end |
66 private :template_path_for | |
62 end | 67 end |
63 | 68 |
64 def info | 69 def info |
65 cmd = "#{HG_BIN} -R #{target('')} root" | 70 tip = summary['tip'].first |
66 root_url = nil | 71 Info.new(:root_url => summary['root'].first['path'], |
67 shellout(cmd) do |io| | 72 :lastrev => Revision.new(:identifier => tip['rev'].to_i, |
68 root_url = io.read | 73 :revision => tip['rev'], |
69 end | 74 :scmid => tip['node'])) |
70 return nil if $? && $?.exitstatus != 0 | 75 end |
71 info = Info.new({:root_url => root_url.chomp, | 76 |
72 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last | 77 def tags |
73 }) | 78 summary['tags'].map { |e| e['name'] } |
74 info | 79 end |
75 rescue CommandFailed | 80 |
76 return nil | 81 # Returns map of {'tag' => 'nodeid', ...} |
77 end | 82 def tagmap |
78 | 83 alist = summary['tags'].map { |e| e.values_at('name', 'node') } |
84 Hash[*alist.flatten] | |
85 end | |
86 | |
87 def branches | |
88 summary['branches'].map { |e| e['name'] } | |
89 end | |
90 | |
91 # Returns map of {'branch' => 'nodeid', ...} | |
92 def branchmap | |
93 alist = summary['branches'].map { |e| e.values_at('name', 'node') } | |
94 Hash[*alist.flatten] | |
95 end | |
96 | |
97 # NOTE: DO NOT IMPLEMENT default_branch !! | |
98 # It's used as the default revision by RepositoriesController. | |
99 | |
100 def summary | |
101 @summary ||= fetchg 'rhsummary' | |
102 end | |
103 private :summary | |
104 | |
79 def entries(path=nil, identifier=nil) | 105 def entries(path=nil, identifier=nil) |
80 path ||= '' | |
81 entries = Entries.new | 106 entries = Entries.new |
82 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" | 107 fetched_entries = fetchg('rhentries', '-r', hgrev(identifier), |
83 cmd << " -r " + (identifier ? identifier.to_s : "tip") | 108 without_leading_slash(path.to_s)) |
84 cmd << " " + shell_quote("path:#{path}") unless path.empty? | 109 |
85 shellout(cmd) do |io| | 110 fetched_entries['dirs'].each do |e| |
86 io.each_line do |line| | 111 entries << Entry.new(:name => e['name'], |
87 # HG uses antislashs as separator on Windows | 112 :path => "#{with_trailling_slash(path)}#{e['name']}", |
88 line = line.gsub(/\\/, "/") | 113 :kind => 'dir') |
89 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') | 114 end |
90 e ||= line | 115 |
91 e = e.chomp.split(%r{[\/\\]}) | 116 fetched_entries['files'].each do |e| |
92 entries << Entry.new({:name => e.first, | 117 entries << Entry.new(:name => e['name'], |
93 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), | 118 :path => "#{with_trailling_slash(path)}#{e['name']}", |
94 :kind => (e.size > 1 ? 'dir' : 'file'), | 119 :kind => 'file', |
95 :lastrev => Revision.new | 120 :size => e['size'].to_i, |
96 }) unless e.empty? || entries.detect{|entry| entry.name == e.first} | 121 :lastrev => Revision.new(:identifier => e['rev'].to_i, |
122 :time => Time.at(e['time'].to_i))) | |
123 end | |
124 | |
125 entries | |
126 rescue HgCommandAborted | |
127 nil # means not found | |
128 end | |
129 | |
130 # TODO: is this api necessary? | |
131 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) | |
132 revisions = Revisions.new | |
133 each_revision { |e| revisions << e } | |
134 revisions | |
135 end | |
136 | |
137 # Iterates the revisions by using a template file that | |
138 # makes Mercurial produce a xml output. | |
139 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) | |
140 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] | |
141 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" | |
142 hg_args << '--limit' << options[:limit] if options[:limit] | |
143 hg_args << without_leading_slash(path) unless path.blank? | |
144 doc = hg(*hg_args) { |io| REXML::Document.new(io.read) } | |
145 # TODO: ??? HG doesn't close the XML Document... | |
146 | |
147 doc.each_element('log/logentry') do |le| | |
148 cpalist = le.get_elements('paths/path-copied').map do |e| | |
149 [e.text, e.attributes['copyfrom-path']] | |
150 end | |
151 cpmap = Hash[*cpalist.flatten] | |
152 | |
153 paths = le.get_elements('paths/path').map do |e| | |
154 {:action => e.attributes['action'], :path => with_leading_slash(e.text), | |
155 :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil), | |
156 :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)} | |
157 end.sort { |a, b| a[:path] <=> b[:path] } | |
158 | |
159 yield Revision.new(:identifier => le.attributes['revision'], | |
160 :revision => le.attributes['revision'], | |
161 :scmid => le.attributes['node'], | |
162 :author => (le.elements['author'].text rescue ''), | |
163 :time => Time.parse(le.elements['date'].text).localtime, | |
164 :message => le.elements['msg'].text, | |
165 :paths => paths) | |
166 end | |
167 self | |
168 end | |
169 | |
170 # Returns list of nodes in the specified branch | |
171 def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={}) | |
172 hg_args = ['log', '--template', '{node|short}\n', '-b', branch] | |
173 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" | |
174 hg_args << '--limit' << options[:limit] if options[:limit] | |
175 hg_args << without_leading_slash(path) unless path.blank? | |
176 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } | |
177 end | |
178 | |
179 def diff(path, identifier_from, identifier_to=nil) | |
180 hg_args = ['diff', '--nodates'] | |
181 if identifier_to | |
182 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) | |
183 else | |
184 hg_args << '-c' << hgrev(identifier_from) | |
185 end | |
186 hg_args << without_leading_slash(path) unless path.blank? | |
187 | |
188 hg *hg_args do |io| | |
189 io.collect | |
190 end | |
191 rescue HgCommandAborted | |
192 nil # means not found | |
193 end | |
194 | |
195 def cat(path, identifier=nil) | |
196 hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io| | |
197 io.binmode | |
198 io.read | |
199 end | |
200 rescue HgCommandAborted | |
201 nil # means not found | |
202 end | |
203 | |
204 def annotate(path, identifier=nil) | |
205 blame = Annotate.new | |
206 hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io| | |
207 io.each do |line| | |
208 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$} | |
209 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3) | |
210 blame.add_line($4.rstrip, r) | |
211 end | |
212 end | |
213 blame | |
214 rescue HgCommandAborted | |
215 nil # means not found or cannot be annotated | |
216 end | |
217 | |
218 # Runs 'hg' command with the given args | |
219 def hg(*args, &block) | |
220 full_args = [HG_BIN, '--cwd', url] | |
221 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" | |
222 full_args += args | |
223 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) | |
224 if $? && $?.exitstatus != 0 | |
225 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" | |
226 end | |
227 ret | |
228 end | |
229 private :hg | |
230 | |
231 # Runs 'hg' helper, then parses output to return | |
232 def fetchg(*args) | |
233 # command output example: | |
234 # :tip: rev node | |
235 # 100 abcdef012345 | |
236 # :tags: rev node name | |
237 # 100 abcdef012345 tip | |
238 # ... | |
239 data = Hash.new { |h, k| h[k] = [] } | |
240 hg(*args) do |io| | |
241 key, attrs = nil, nil | |
242 io.each do |line| | |
243 next if line.chomp.empty? | |
244 if /^:(\w+): ([\w ]+)/ =~ line | |
245 key = $1 | |
246 attrs = $2.split(/ /) | |
247 elsif key | |
248 alist = attrs.zip(line.chomp.split(/ /, attrs.size)) | |
249 data[key] << Hash[*alist.flatten] | |
97 end | 250 end |
98 end | 251 end |
99 end | 252 end |
100 return nil if $? && $?.exitstatus != 0 | 253 data |
101 entries.sort_by_name | 254 end |
102 end | 255 private :fetchg |
103 | 256 |
104 # Fetch the revisions by using a template file that | 257 # Returns correct revision identifier |
105 # makes Mercurial produce a xml output. | 258 def hgrev(identifier) |
106 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) | 259 identifier.blank? ? 'tip' : identifier.to_s |
107 revisions = Revisions.new | 260 end |
108 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}" | 261 private :hgrev |
109 if identifier_from && identifier_to | |
110 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" | |
111 elsif identifier_from | |
112 cmd << " -r #{identifier_from.to_i}:" | |
113 end | |
114 cmd << " --limit #{options[:limit].to_i}" if options[:limit] | |
115 cmd << " #{path}" if path | |
116 shellout(cmd) do |io| | |
117 begin | |
118 # HG doesn't close the XML Document... | |
119 doc = REXML::Document.new(io.read << "</log>") | |
120 doc.elements.each("log/logentry") do |logentry| | |
121 paths = [] | |
122 copies = logentry.get_elements('paths/path-copied') | |
123 logentry.elements.each("paths/path") do |path| | |
124 # Detect if the added file is a copy | |
125 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } | |
126 from_path = c.attributes['copyfrom-path'] | |
127 from_rev = logentry.attributes['revision'] | |
128 end | |
129 paths << {:action => path.attributes['action'], | |
130 :path => "/#{path.text}", | |
131 :from_path => from_path ? "/#{from_path}" : nil, | |
132 :from_revision => from_rev ? from_rev : nil | |
133 } | |
134 end | |
135 paths.sort! { |x,y| x[:path] <=> y[:path] } | |
136 | |
137 revisions << Revision.new({:identifier => logentry.attributes['revision'], | |
138 :scmid => logentry.attributes['node'], | |
139 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), | |
140 :time => Time.parse(logentry.elements['date'].text).localtime, | |
141 :message => logentry.elements['msg'].text, | |
142 :paths => paths | |
143 }) | |
144 end | |
145 rescue | |
146 logger.debug($!) | |
147 end | |
148 end | |
149 return nil if $? && $?.exitstatus != 0 | |
150 revisions | |
151 end | |
152 | |
153 def diff(path, identifier_from, identifier_to=nil) | |
154 path ||= '' | |
155 if identifier_to | |
156 identifier_to = identifier_to.to_i | |
157 else | |
158 identifier_to = identifier_from.to_i - 1 | |
159 end | |
160 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates" | |
161 cmd << " -I #{target(path)}" unless path.empty? | |
162 diff = [] | |
163 shellout(cmd) do |io| | |
164 io.each_line do |line| | |
165 diff << line | |
166 end | |
167 end | |
168 return nil if $? && $?.exitstatus != 0 | |
169 diff | |
170 end | |
171 | |
172 def cat(path, identifier=nil) | |
173 cmd = "#{HG_BIN} -R #{target('')} cat" | |
174 cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
175 cmd << " #{target(path)}" | |
176 cat = nil | |
177 shellout(cmd) do |io| | |
178 io.binmode | |
179 cat = io.read | |
180 end | |
181 return nil if $? && $?.exitstatus != 0 | |
182 cat | |
183 end | |
184 | |
185 def annotate(path, identifier=nil) | |
186 path ||= '' | |
187 cmd = "#{HG_BIN} -R #{target('')}" | |
188 cmd << " annotate -n -u" | |
189 cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
190 cmd << " -r #{identifier.to_i}" if identifier | |
191 cmd << " #{target(path)}" | |
192 blame = Annotate.new | |
193 shellout(cmd) do |io| | |
194 io.each_line do |line| | |
195 next unless line =~ %r{^([^:]+)\s(\d+):(.*)$} | |
196 blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip)) | |
197 end | |
198 end | |
199 return nil if $? && $?.exitstatus != 0 | |
200 blame | |
201 end | |
202 end | 262 end |
203 end | 263 end |
204 end | 264 end |
205 end | 265 end |