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@3
|
159 yield Revision.new(:identifier => le.attributes['revision'],
|
Chris@3
|
160 :revision => le.attributes['revision'],
|
Chris@3
|
161 :scmid => le.attributes['node'],
|
Chris@3
|
162 :author => (le.elements['author'].text rescue ''),
|
Chris@3
|
163 :time => Time.parse(le.elements['date'].text).localtime,
|
Chris@3
|
164 :message => le.elements['msg'].text,
|
Chris@3
|
165 :paths => paths)
|
Chris@3
|
166 end
|
Chris@3
|
167 self
|
Chris@3
|
168 end
|
Chris@3
|
169
|
Chris@3
|
170 # Returns list of nodes in the specified branch
|
Chris@3
|
171 def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={})
|
Chris@3
|
172 hg_args = ['log', '--template', '{node|short}\n', '-b', branch]
|
Chris@3
|
173 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
|
Chris@3
|
174 hg_args << '--limit' << options[:limit] if options[:limit]
|
Chris@3
|
175 hg_args << without_leading_slash(path) unless path.blank?
|
Chris@3
|
176 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
|
Chris@3
|
177 end
|
Chris@3
|
178
|
Chris@3
|
179 def diff(path, identifier_from, identifier_to=nil)
|
Chris@3
|
180 hg_args = ['diff', '--nodates']
|
Chris@3
|
181 if identifier_to
|
Chris@3
|
182 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
|
Chris@3
|
183 else
|
Chris@3
|
184 hg_args << '-c' << hgrev(identifier_from)
|
Chris@3
|
185 end
|
Chris@3
|
186 hg_args << without_leading_slash(path) unless path.blank?
|
Chris@3
|
187
|
Chris@3
|
188 hg *hg_args do |io|
|
Chris@3
|
189 io.collect
|
Chris@3
|
190 end
|
Chris@3
|
191 rescue HgCommandAborted
|
Chris@3
|
192 nil # means not found
|
Chris@3
|
193 end
|
Chris@3
|
194
|
Chris@3
|
195 def cat(path, identifier=nil)
|
Chris@3
|
196 hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io|
|
Chris@3
|
197 io.binmode
|
Chris@3
|
198 io.read
|
Chris@3
|
199 end
|
Chris@3
|
200 rescue HgCommandAborted
|
Chris@3
|
201 nil # means not found
|
Chris@3
|
202 end
|
Chris@3
|
203
|
Chris@3
|
204 def annotate(path, identifier=nil)
|
Chris@3
|
205 blame = Annotate.new
|
Chris@3
|
206 hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io|
|
Chris@3
|
207 io.each do |line|
|
Chris@3
|
208 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$}
|
Chris@3
|
209 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3)
|
Chris@3
|
210 blame.add_line($4.rstrip, r)
|
Chris@3
|
211 end
|
Chris@3
|
212 end
|
Chris@3
|
213 blame
|
Chris@3
|
214 rescue HgCommandAborted
|
Chris@3
|
215 nil # means not found or cannot be annotated
|
Chris@3
|
216 end
|
Chris@3
|
217
|
Chris@3
|
218 # Runs 'hg' command with the given args
|
Chris@3
|
219 def hg(*args, &block)
|
Chris@3
|
220 full_args = [HG_BIN, '--cwd', url]
|
Chris@3
|
221 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
|
Chris@3
|
222 full_args += args
|
Chris@3
|
223 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
|
Chris@3
|
224 if $? && $?.exitstatus != 0
|
Chris@3
|
225 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
|
Chris@3
|
226 end
|
Chris@3
|
227 ret
|
Chris@3
|
228 end
|
Chris@3
|
229 private :hg
|
Chris@3
|
230
|
Chris@3
|
231 # Runs 'hg' helper, then parses output to return
|
Chris@3
|
232 def fetchg(*args)
|
Chris@3
|
233 # command output example:
|
Chris@3
|
234 # :tip: rev node
|
Chris@3
|
235 # 100 abcdef012345
|
Chris@3
|
236 # :tags: rev node name
|
Chris@3
|
237 # 100 abcdef012345 tip
|
Chris@3
|
238 # ...
|
Chris@3
|
239 data = Hash.new { |h, k| h[k] = [] }
|
Chris@3
|
240 hg(*args) do |io|
|
Chris@3
|
241 key, attrs = nil, nil
|
Chris@3
|
242 io.each do |line|
|
Chris@3
|
243 next if line.chomp.empty?
|
Chris@3
|
244 if /^:(\w+): ([\w ]+)/ =~ line
|
Chris@3
|
245 key = $1
|
Chris@3
|
246 attrs = $2.split(/ /)
|
Chris@3
|
247 elsif key
|
Chris@3
|
248 alist = attrs.zip(line.chomp.split(/ /, attrs.size))
|
Chris@3
|
249 data[key] << Hash[*alist.flatten]
|
Chris@0
|
250 end
|
Chris@0
|
251 end
|
Chris@0
|
252 end
|
Chris@3
|
253 data
|
Chris@0
|
254 end
|
Chris@3
|
255 private :fetchg
|
Chris@3
|
256
|
Chris@3
|
257 # Returns correct revision identifier
|
Chris@3
|
258 def hgrev(identifier)
|
Chris@3
|
259 identifier.blank? ? 'tip' : identifier.to_s
|
Chris@0
|
260 end
|
Chris@3
|
261 private :hgrev
|
Chris@0
|
262 end
|
Chris@0
|
263 end
|
Chris@0
|
264 end
|
Chris@0
|
265 end
|