Chris@0
|
1 # redMine - project management software
|
Chris@0
|
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
|
Chris@0
|
3 #
|
Chris@0
|
4 # This program is free software; you can redistribute it and/or
|
Chris@0
|
5 # modify it under the terms of the GNU General Public License
|
Chris@0
|
6 # as published by the Free Software Foundation; either version 2
|
Chris@0
|
7 # of the License, or (at your option) any later version.
|
Chris@0
|
8 #
|
Chris@0
|
9 # This program is distributed in the hope that it will be useful,
|
Chris@0
|
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
12 # GNU General Public License for more details.
|
Chris@0
|
13 #
|
Chris@0
|
14 # You should have received a copy of the GNU General Public License
|
Chris@0
|
15 # along with this program; if not, write to the Free Software
|
Chris@0
|
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
Chris@0
|
17
|
Chris@0
|
18 require 'redmine/scm/adapters/abstract_adapter'
|
Chris@3
|
19 require 'rexml/document'
|
Chris@0
|
20
|
Chris@0
|
21 module Redmine
|
Chris@0
|
22 module Scm
|
Chris@0
|
23 module Adapters
|
Chris@0
|
24 class MercurialAdapter < AbstractAdapter
|
Chris@0
|
25
|
Chris@0
|
26 # Mercurial executable name
|
Chris@0
|
27 HG_BIN = "hg"
|
Chris@3
|
28 HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py"
|
Chris@0
|
29 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
|
Chris@0
|
30 TEMPLATE_NAME = "hg-template"
|
Chris@0
|
31 TEMPLATE_EXTENSION = "tmpl"
|
Chris@0
|
32
|
Chris@3
|
33 # raised if hg command exited with error, e.g. unknown revision.
|
Chris@3
|
34 class HgCommandAborted < CommandFailed; end
|
Chris@3
|
35
|
Chris@0
|
36 class << self
|
Chris@0
|
37 def client_version
|
Chris@3
|
38 @client_version ||= hgversion
|
Chris@0
|
39 end
|
Chris@0
|
40
|
Chris@0
|
41 def hgversion
|
Chris@0
|
42 # The hg version is expressed either as a
|
Chris@0
|
43 # release number (eg 0.9.5 or 1.0) or as a revision
|
Chris@0
|
44 # id composed of 12 hexa characters.
|
Chris@3
|
45 hgversion_str.to_s.split('.').map { |e| e.to_i }
|
Chris@0
|
46 end
|
Chris@3
|
47 private :hgversion
|
Chris@0
|
48
|
Chris@3
|
49 def hgversion_str
|
Chris@3
|
50 shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/]
|
Chris@0
|
51 end
|
Chris@3
|
52 private :hgversion_str
|
Chris@0
|
53
|
Chris@0
|
54 def template_path
|
Chris@3
|
55 template_path_for(client_version)
|
Chris@0
|
56 end
|
Chris@0
|
57
|
Chris@0
|
58 def template_path_for(version)
|
Chris@0
|
59 if ((version <=> [0,9,5]) > 0) || version.empty?
|
Chris@0
|
60 ver = "1.0"
|
Chris@0
|
61 else
|
Chris@0
|
62 ver = "0.9.5"
|
Chris@0
|
63 end
|
Chris@0
|
64 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
|
Chris@0
|
65 end
|
Chris@3
|
66 private :template_path_for
|
Chris@0
|
67 end
|
Chris@0
|
68
|
Chris@0
|
69 def info
|
Chris@3
|
70 tip = summary['tip'].first
|
Chris@3
|
71 Info.new(:root_url => summary['root'].first['path'],
|
Chris@3
|
72 :lastrev => Revision.new(:identifier => tip['rev'].to_i,
|
Chris@3
|
73 :revision => tip['rev'],
|
Chris@3
|
74 :scmid => tip['node']))
|
Chris@3
|
75 end
|
Chris@3
|
76
|
Chris@3
|
77 def tags
|
Chris@3
|
78 summary['tags'].map { |e| e['name'] }
|
Chris@0
|
79 end
|
Chris@0
|
80
|
Chris@3
|
81 # Returns map of {'tag' => 'nodeid', ...}
|
Chris@3
|
82 def tagmap
|
Chris@3
|
83 alist = summary['tags'].map { |e| e.values_at('name', 'node') }
|
Chris@3
|
84 Hash[*alist.flatten]
|
Chris@3
|
85 end
|
Chris@3
|
86
|
Chris@3
|
87 def branches
|
Chris@3
|
88 summary['branches'].map { |e| e['name'] }
|
Chris@3
|
89 end
|
Chris@3
|
90
|
Chris@3
|
91 # Returns map of {'branch' => 'nodeid', ...}
|
Chris@3
|
92 def branchmap
|
Chris@3
|
93 alist = summary['branches'].map { |e| e.values_at('name', 'node') }
|
Chris@3
|
94 Hash[*alist.flatten]
|
Chris@3
|
95 end
|
Chris@3
|
96
|
Chris@3
|
97 # NOTE: DO NOT IMPLEMENT default_branch !!
|
Chris@3
|
98 # It's used as the default revision by RepositoriesController.
|
Chris@3
|
99
|
Chris@3
|
100 def summary
|
Chris@3
|
101 @summary ||= fetchg 'rhsummary'
|
Chris@3
|
102 end
|
Chris@3
|
103 private :summary
|
Chris@3
|
104
|
Chris@0
|
105 def entries(path=nil, identifier=nil)
|
Chris@0
|
106 entries = Entries.new
|
Chris@3
|
107 fetched_entries = fetchg('rhentries', '-r', hgrev(identifier),
|
Chris@3
|
108 without_leading_slash(path.to_s))
|
Chris@3
|
109
|
Chris@3
|
110 fetched_entries['dirs'].each do |e|
|
Chris@3
|
111 entries << Entry.new(:name => e['name'],
|
Chris@3
|
112 :path => "#{with_trailling_slash(path)}#{e['name']}",
|
Chris@3
|
113 :kind => 'dir')
|
Chris@3
|
114 end
|
Chris@3
|
115
|
Chris@3
|
116 fetched_entries['files'].each do |e|
|
Chris@3
|
117 entries << Entry.new(:name => e['name'],
|
Chris@3
|
118 :path => "#{with_trailling_slash(path)}#{e['name']}",
|
Chris@3
|
119 :kind => 'file',
|
Chris@3
|
120 :size => e['size'].to_i,
|
Chris@3
|
121 :lastrev => Revision.new(:identifier => e['rev'].to_i,
|
Chris@3
|
122 :time => Time.at(e['time'].to_i)))
|
Chris@3
|
123 end
|
Chris@3
|
124
|
Chris@3
|
125 entries
|
Chris@3
|
126 rescue HgCommandAborted
|
Chris@3
|
127 nil # means not found
|
Chris@3
|
128 end
|
Chris@3
|
129
|
Chris@3
|
130 # TODO: is this api necessary?
|
Chris@3
|
131 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
Chris@3
|
132 revisions = Revisions.new
|
Chris@3
|
133 each_revision { |e| revisions << e }
|
Chris@3
|
134 revisions
|
Chris@3
|
135 end
|
Chris@3
|
136
|
Chris@3
|
137 # Iterates the revisions by using a template file that
|
Chris@3
|
138 # makes Mercurial produce a xml output.
|
Chris@3
|
139 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
Chris@3
|
140 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
|
Chris@3
|
141 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
|
Chris@3
|
142 hg_args << '--limit' << options[:limit] if options[:limit]
|
Chris@3
|
143 hg_args << without_leading_slash(path) unless path.blank?
|
Chris@11
|
144 doc = hg(*hg_args) { |io| REXML::Document.new(io.read) }
|
Chris@3
|
145 # TODO: ??? HG doesn't close the XML Document...
|
Chris@3
|
146
|
Chris@3
|
147 doc.each_element('log/logentry') do |le|
|
Chris@3
|
148 cpalist = le.get_elements('paths/path-copied').map do |e|
|
Chris@3
|
149 [e.text, e.attributes['copyfrom-path']]
|
Chris@3
|
150 end
|
Chris@3
|
151 cpmap = Hash[*cpalist.flatten]
|
Chris@3
|
152
|
Chris@3
|
153 paths = le.get_elements('paths/path').map do |e|
|
Chris@3
|
154 {:action => e.attributes['action'], :path => with_leading_slash(e.text),
|
Chris@3
|
155 :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil),
|
Chris@3
|
156 :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)}
|
Chris@3
|
157 end.sort { |a, b| a[:path] <=> b[:path] }
|
Chris@3
|
158
|
chris@36
|
159 branch = le.elements['branch'].text;
|
chris@36
|
160 logger.debug("Branch is #{branch}");
|
chris@36
|
161
|
Chris@3
|
162 yield Revision.new(:identifier => le.attributes['revision'],
|
Chris@3
|
163 :revision => le.attributes['revision'],
|
Chris@3
|
164 :scmid => le.attributes['node'],
|
Chris@3
|
165 :author => (le.elements['author'].text rescue ''),
|
Chris@3
|
166 :time => Time.parse(le.elements['date'].text).localtime,
|
Chris@3
|
167 :message => le.elements['msg'].text,
|
chris@36
|
168 :branch => le.elements['branch'].text,
|
Chris@3
|
169 :paths => paths)
|
Chris@3
|
170 end
|
Chris@3
|
171 self
|
Chris@3
|
172 end
|
Chris@3
|
173
|
Chris@3
|
174 # Returns list of nodes in the specified branch
|
Chris@3
|
175 def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={})
|
chris@36
|
176 logger.debug("nodes_in_branch: Branch is #{branch}");
|
Chris@3
|
177 hg_args = ['log', '--template', '{node|short}\n', '-b', branch]
|
Chris@3
|
178 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
|
Chris@3
|
179 hg_args << '--limit' << options[:limit] if options[:limit]
|
Chris@3
|
180 hg_args << without_leading_slash(path) unless path.blank?
|
Chris@3
|
181 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
|
Chris@3
|
182 end
|
Chris@3
|
183
|
Chris@3
|
184 def diff(path, identifier_from, identifier_to=nil)
|
Chris@3
|
185 hg_args = ['diff', '--nodates']
|
Chris@3
|
186 if identifier_to
|
Chris@3
|
187 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
|
Chris@3
|
188 else
|
Chris@3
|
189 hg_args << '-c' << hgrev(identifier_from)
|
Chris@3
|
190 end
|
Chris@3
|
191 hg_args << without_leading_slash(path) unless path.blank?
|
Chris@3
|
192
|
Chris@3
|
193 hg *hg_args do |io|
|
Chris@3
|
194 io.collect
|
Chris@3
|
195 end
|
Chris@3
|
196 rescue HgCommandAborted
|
Chris@3
|
197 nil # means not found
|
Chris@3
|
198 end
|
Chris@3
|
199
|
Chris@3
|
200 def cat(path, identifier=nil)
|
Chris@3
|
201 hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
|
Chris@3
|
202 io.binmode
|
Chris@3
|
203 io.read
|
Chris@3
|
204 end
|
Chris@3
|
205 rescue HgCommandAborted
|
Chris@3
|
206 nil # means not found
|
Chris@3
|
207 end
|
Chris@3
|
208
|
Chris@3
|
209 def annotate(path, identifier=nil)
|
Chris@3
|
210 blame = Annotate.new
|
Chris@3
|
211 hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io|
|
Chris@3
|
212 io.each do |line|
|
Chris@3
|
213 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$}
|
Chris@3
|
214 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3)
|
Chris@3
|
215 blame.add_line($4.rstrip, r)
|
Chris@3
|
216 end
|
Chris@3
|
217 end
|
Chris@3
|
218 blame
|
Chris@3
|
219 rescue HgCommandAborted
|
Chris@3
|
220 nil # means not found or cannot be annotated
|
Chris@3
|
221 end
|
Chris@3
|
222
|
Chris@3
|
223 # Runs 'hg' command with the given args
|
Chris@3
|
224 def hg(*args, &block)
|
Chris@3
|
225 full_args = [HG_BIN, '--cwd', url]
|
Chris@3
|
226 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
|
Chris@3
|
227 full_args += args
|
Chris@3
|
228 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
|
Chris@3
|
229 if $? && $?.exitstatus != 0
|
Chris@3
|
230 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
|
Chris@3
|
231 end
|
Chris@3
|
232 ret
|
Chris@3
|
233 end
|
Chris@3
|
234 private :hg
|
Chris@3
|
235
|
Chris@3
|
236 # Runs 'hg' helper, then parses output to return
|
Chris@3
|
237 def fetchg(*args)
|
Chris@3
|
238 # command output example:
|
Chris@3
|
239 # :tip: rev node
|
Chris@3
|
240 # 100 abcdef012345
|
Chris@3
|
241 # :tags: rev node name
|
Chris@3
|
242 # 100 abcdef012345 tip
|
Chris@3
|
243 # ...
|
Chris@3
|
244 data = Hash.new { |h, k| h[k] = [] }
|
Chris@3
|
245 hg(*args) do |io|
|
Chris@3
|
246 key, attrs = nil, nil
|
Chris@3
|
247 io.each do |line|
|
Chris@3
|
248 next if line.chomp.empty?
|
Chris@3
|
249 if /^:(\w+): ([\w ]+)/ =~ line
|
Chris@3
|
250 key = $1
|
Chris@3
|
251 attrs = $2.split(/ /)
|
Chris@3
|
252 elsif key
|
Chris@3
|
253 alist = attrs.zip(line.chomp.split(/ /, attrs.size))
|
Chris@3
|
254 data[key] << Hash[*alist.flatten]
|
Chris@0
|
255 end
|
Chris@0
|
256 end
|
Chris@0
|
257 end
|
Chris@3
|
258 data
|
Chris@0
|
259 end
|
Chris@3
|
260 private :fetchg
|
Chris@3
|
261
|
Chris@3
|
262 # Returns correct revision identifier
|
Chris@3
|
263 def hgrev(identifier)
|
Chris@3
|
264 identifier.blank? ? 'tip' : identifier.to_s
|
Chris@0
|
265 end
|
Chris@3
|
266 private :hgrev
|
Chris@0
|
267 end
|
Chris@0
|
268 end
|
Chris@0
|
269 end
|
Chris@0
|
270 end
|