To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / lib / redmine / scm / adapters / mercurial_adapter.rb @ 912:5e80956cc792
History | View | Annotate | Download (12 KB)
| 1 | 441:cbce1fd3b1b7 | Chris | # Redmine - project management software
|
|---|---|---|---|
| 2 | # Copyright (C) 2006-2011 Jean-Philippe Lang
|
||
| 3 | 0:513646585e45 | Chris | #
|
| 4 | # This program is free software; you can redistribute it and/or
|
||
| 5 | # modify it under the terms of the GNU General Public License
|
||
| 6 | # as published by the Free Software Foundation; either version 2
|
||
| 7 | # of the License, or (at your option) any later version.
|
||
| 8 | 441:cbce1fd3b1b7 | Chris | #
|
| 9 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful,
|
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
| 12 | # GNU General Public License for more details.
|
||
| 13 | 441:cbce1fd3b1b7 | Chris | #
|
| 14 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License
|
| 15 | # along with this program; if not, write to the Free Software
|
||
| 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
| 17 | |||
| 18 | require 'redmine/scm/adapters/abstract_adapter'
|
||
| 19 | 119:8661b858af72 | Chris | require 'cgi'
|
| 20 | 0:513646585e45 | Chris | |
| 21 | module Redmine |
||
| 22 | module Scm |
||
| 23 | 245:051f544170fe | Chris | module Adapters |
| 24 | 0:513646585e45 | Chris | class MercurialAdapter < AbstractAdapter |
| 25 | 119:8661b858af72 | Chris | |
| 26 | 0:513646585e45 | Chris | # Mercurial executable name
|
| 27 | 210:0579821a129a | Chris | HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg" |
| 28 | 245:051f544170fe | Chris | HELPERS_DIR = File.dirname(__FILE__) + "/mercurial" |
| 29 | HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py" |
||
| 30 | 0:513646585e45 | Chris | TEMPLATE_NAME = "hg-template" |
| 31 | TEMPLATE_EXTENSION = "tmpl" |
||
| 32 | 119:8661b858af72 | Chris | |
| 33 | 245:051f544170fe | Chris | # raised if hg command exited with error, e.g. unknown revision.
|
| 34 | class HgCommandAborted < CommandFailed; end |
||
| 35 | |||
| 36 | 0:513646585e45 | Chris | class << self |
| 37 | 245:051f544170fe | Chris | def client_command |
| 38 | @@bin ||= HG_BIN |
||
| 39 | end
|
||
| 40 | |||
| 41 | def sq_bin |
||
| 42 | 909:cbb26bc654de | Chris | @@sq_bin ||= shell_quote_command
|
| 43 | 245:051f544170fe | Chris | end
|
| 44 | |||
| 45 | 0:513646585e45 | Chris | def client_version |
| 46 | @@client_version ||= (hgversion || [])
|
||
| 47 | end
|
||
| 48 | 119:8661b858af72 | Chris | |
| 49 | 245:051f544170fe | Chris | def client_available |
| 50 | 909:cbb26bc654de | Chris | client_version_above?([1, 2]) |
| 51 | 245:051f544170fe | Chris | end
|
| 52 | |||
| 53 | def hgversion |
||
| 54 | 0:513646585e45 | Chris | # The hg version is expressed either as a
|
| 55 | # release number (eg 0.9.5 or 1.0) or as a revision
|
||
| 56 | # id composed of 12 hexa characters.
|
||
| 57 | 245:051f544170fe | Chris | theversion = hgversion_from_command_line.dup |
| 58 | if theversion.respond_to?(:force_encoding) |
||
| 59 | theversion.force_encoding('ASCII-8BIT')
|
||
| 60 | end
|
||
| 61 | 119:8661b858af72 | Chris | if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)}) |
| 62 | m[2].scan(%r{\d+}).collect(&:to_i) |
||
| 63 | 0:513646585e45 | Chris | end
|
| 64 | end
|
||
| 65 | 119:8661b858af72 | Chris | |
| 66 | 0:513646585e45 | Chris | def hgversion_from_command_line |
| 67 | 245:051f544170fe | Chris | shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
| 68 | 0:513646585e45 | Chris | end
|
| 69 | 119:8661b858af72 | Chris | |
| 70 | 0:513646585e45 | Chris | def template_path |
| 71 | @@template_path ||= template_path_for(client_version)
|
||
| 72 | end
|
||
| 73 | 119:8661b858af72 | Chris | |
| 74 | 0:513646585e45 | Chris | def template_path_for(version) |
| 75 | 909:cbb26bc654de | Chris | "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
|
| 76 | 0:513646585e45 | Chris | end
|
| 77 | end
|
||
| 78 | 119:8661b858af72 | Chris | |
| 79 | 245:051f544170fe | Chris | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
| 80 | super
|
||
| 81 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 82 | end
|
||
| 83 | |||
| 84 | def path_encoding |
||
| 85 | @path_encoding
|
||
| 86 | 0:513646585e45 | Chris | end
|
| 87 | 119:8661b858af72 | Chris | |
| 88 | 245:051f544170fe | Chris | def info |
| 89 | tip = summary['repository']['tip'] |
||
| 90 | Info.new(:root_url => CGI.unescape(summary['repository']['root']), |
||
| 91 | :lastrev => Revision.new(:revision => tip['revision'], |
||
| 92 | :scmid => tip['node'])) |
||
| 93 | 507:0c939c159af4 | Chris | # rescue HgCommandAborted
|
| 94 | rescue Exception => e |
||
| 95 | logger.error "hg: error during getting info: #{e.message}"
|
||
| 96 | nil
|
||
| 97 | 245:051f544170fe | Chris | end
|
| 98 | |||
| 99 | def tags |
||
| 100 | as_ary(summary['repository']['tag']).map { |e| e['name'] } |
||
| 101 | end
|
||
| 102 | |||
| 103 | # Returns map of {'tag' => 'nodeid', ...}
|
||
| 104 | def tagmap |
||
| 105 | alist = as_ary(summary['repository']['tag']).map do |e| |
||
| 106 | e.values_at('name', 'node') |
||
| 107 | end
|
||
| 108 | Hash[*alist.flatten]
|
||
| 109 | end
|
||
| 110 | |||
| 111 | def branches |
||
| 112 | 909:cbb26bc654de | Chris | brs = [] |
| 113 | as_ary(summary['repository']['branch']).each do |e| |
||
| 114 | br = Branch.new(e['name']) |
||
| 115 | br.revision = e['revision']
|
||
| 116 | br.scmid = e['node']
|
||
| 117 | brs << br |
||
| 118 | end
|
||
| 119 | brs |
||
| 120 | 245:051f544170fe | Chris | end
|
| 121 | |||
| 122 | # Returns map of {'branch' => 'nodeid', ...}
|
||
| 123 | def branchmap |
||
| 124 | alist = as_ary(summary['repository']['branch']).map do |e| |
||
| 125 | e.values_at('name', 'node') |
||
| 126 | end
|
||
| 127 | Hash[*alist.flatten]
|
||
| 128 | end
|
||
| 129 | |||
| 130 | def summary |
||
| 131 | 441:cbce1fd3b1b7 | Chris | return @summary if @summary |
| 132 | 245:051f544170fe | Chris | hg 'rhsummary' do |io| |
| 133 | output = io.read |
||
| 134 | if output.respond_to?(:force_encoding) |
||
| 135 | output.force_encoding('UTF-8')
|
||
| 136 | end
|
||
| 137 | begin
|
||
| 138 | @summary = ActiveSupport::XmlMini.parse(output)['rhsummary'] |
||
| 139 | rescue
|
||
| 140 | 0:513646585e45 | Chris | end
|
| 141 | end
|
||
| 142 | 245:051f544170fe | Chris | end
|
| 143 | private :summary
|
||
| 144 | |||
| 145 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={}) |
| 146 | 245:051f544170fe | Chris | p1 = scm_iconv(@path_encoding, 'UTF-8', path) |
| 147 | manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)), |
||
| 148 | CGI.escape(without_leading_slash(p1.to_s))) do |io| |
||
| 149 | output = io.read |
||
| 150 | if output.respond_to?(:force_encoding) |
||
| 151 | output.force_encoding('UTF-8')
|
||
| 152 | end
|
||
| 153 | begin
|
||
| 154 | ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest'] |
||
| 155 | rescue
|
||
| 156 | end
|
||
| 157 | end
|
||
| 158 | path_prefix = path.blank? ? '' : with_trailling_slash(path)
|
||
| 159 | |||
| 160 | entries = Entries.new
|
||
| 161 | as_ary(manifest['dir']).each do |e| |
||
| 162 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) |
||
| 163 | p = "#{path_prefix}#{n}"
|
||
| 164 | entries << Entry.new(:name => n, :path => p, :kind => 'dir') |
||
| 165 | end
|
||
| 166 | |||
| 167 | as_ary(manifest['file']).each do |e| |
||
| 168 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) |
||
| 169 | p = "#{path_prefix}#{n}"
|
||
| 170 | lr = Revision.new(:revision => e['revision'], :scmid => e['node'], |
||
| 171 | :identifier => e['node'], |
||
| 172 | :time => Time.at(e['time'].to_i)) |
||
| 173 | entries << Entry.new(:name => n, :path => p, :kind => 'file', |
||
| 174 | :size => e['size'].to_i, :lastrev => lr) |
||
| 175 | end
|
||
| 176 | |||
| 177 | entries |
||
| 178 | rescue HgCommandAborted |
||
| 179 | nil # means not found |
||
| 180 | 0:513646585e45 | Chris | end
|
| 181 | 119:8661b858af72 | Chris | |
| 182 | 245:051f544170fe | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) |
| 183 | revs = Revisions.new
|
||
| 184 | each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
|
||
| 185 | revs |
||
| 186 | end
|
||
| 187 | |||
| 188 | # Iterates the revisions by using a template file that
|
||
| 189 | 0:513646585e45 | Chris | # makes Mercurial produce a xml output.
|
| 190 | 245:051f544170fe | Chris | def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) |
| 191 | hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] |
||
| 192 | hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" |
||
| 193 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 194 | hg_args << hgtarget(path) unless path.blank?
|
||
| 195 | log = hg(*hg_args) do |io|
|
||
| 196 | output = io.read |
||
| 197 | if output.respond_to?(:force_encoding) |
||
| 198 | output.force_encoding('UTF-8')
|
||
| 199 | end
|
||
| 200 | 0:513646585e45 | Chris | begin
|
| 201 | 251:c2ffbc10233e | chris | ActiveSupport::XmlMini.parse("#{output}")['log'] |
| 202 | 0:513646585e45 | Chris | rescue
|
| 203 | end
|
||
| 204 | end
|
||
| 205 | 245:051f544170fe | Chris | as_ary(log['logentry']).each do |le| |
| 206 | cpalist = as_ary(le['paths']['path-copied']).map do |e| |
||
| 207 | 441:cbce1fd3b1b7 | Chris | [e['__content__'], e['copyfrom-path']].map do |s| |
| 208 | scm_iconv('UTF-8', @path_encoding, CGI.unescape(s)) |
||
| 209 | end
|
||
| 210 | 245:051f544170fe | Chris | end
|
| 211 | cpmap = Hash[*cpalist.flatten]
|
||
| 212 | paths = as_ary(le['paths']['path']).map do |e| |
||
| 213 | p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) ) |
||
| 214 | 441:cbce1fd3b1b7 | Chris | {:action => e['action'],
|
| 215 | :path => with_leading_slash(p),
|
||
| 216 | :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), |
||
| 217 | :from_revision => (cpmap.member?(p) ? le['node'] : nil)} |
||
| 218 | 245:051f544170fe | Chris | end.sort { |a, b| a[:path] <=> b[:path] } |
| 219 | 909:cbb26bc654de | Chris | parents_ary = [] |
| 220 | as_ary(le['parents']['parent']).map do |par| |
||
| 221 | parents_ary << par['__content__'] if par['__content__'] != "000000000000" |
||
| 222 | end
|
||
| 223 | 245:051f544170fe | Chris | yield Revision.new(:revision => le['revision'], |
| 224 | 441:cbce1fd3b1b7 | Chris | :scmid => le['node'], |
| 225 | :author => (le['author']['__content__'] rescue ''), |
||
| 226 | :time => Time.parse(le['date']['__content__']), |
||
| 227 | :message => le['msg']['__content__'], |
||
| 228 | 909:cbb26bc654de | Chris | :paths => paths,
|
| 229 | :parents => parents_ary)
|
||
| 230 | 245:051f544170fe | Chris | end
|
| 231 | self
|
||
| 232 | 0:513646585e45 | Chris | end
|
| 233 | 119:8661b858af72 | Chris | |
| 234 | 441:cbce1fd3b1b7 | Chris | # Returns list of nodes in the specified branch
|
| 235 | def nodes_in_branch(branch, options={}) |
||
| 236 | hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)] |
||
| 237 | hg_args << '--from' << CGI.escape(branch) |
||
| 238 | hg_args << '--to' << '0' |
||
| 239 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 240 | hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
|
||
| 241 | end
|
||
| 242 | |||
| 243 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 244 | 245:051f544170fe | Chris | hg_args = %w|rhdiff|
|
| 245 | if identifier_to
|
||
| 246 | hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) |
||
| 247 | else
|
||
| 248 | hg_args << '-c' << hgrev(identifier_from)
|
||
| 249 | end
|
||
| 250 | unless path.blank?
|
||
| 251 | p = scm_iconv(@path_encoding, 'UTF-8', path) |
||
| 252 | hg_args << CGI.escape(hgtarget(p))
|
||
| 253 | end
|
||
| 254 | 119:8661b858af72 | Chris | diff = [] |
| 255 | 245:051f544170fe | Chris | hg *hg_args do |io|
|
| 256 | 0:513646585e45 | Chris | io.each_line do |line|
|
| 257 | diff << line |
||
| 258 | end
|
||
| 259 | end
|
||
| 260 | diff |
||
| 261 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 262 | nil # means not found |
||
| 263 | 0:513646585e45 | Chris | end
|
| 264 | 119:8661b858af72 | Chris | |
| 265 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 266 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 267 | 441:cbce1fd3b1b7 | Chris | hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 268 | 0:513646585e45 | Chris | io.binmode |
| 269 | 245:051f544170fe | Chris | io.read |
| 270 | 0:513646585e45 | Chris | end
|
| 271 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 272 | nil # means not found |
||
| 273 | 0:513646585e45 | Chris | end
|
| 274 | 119:8661b858af72 | Chris | |
| 275 | 0:513646585e45 | Chris | def annotate(path, identifier=nil) |
| 276 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 277 | 0:513646585e45 | Chris | blame = Annotate.new
|
| 278 | 441:cbce1fd3b1b7 | Chris | hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 279 | 0:513646585e45 | Chris | io.each_line do |line|
|
| 280 | 245:051f544170fe | Chris | line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding) |
| 281 | 119:8661b858af72 | Chris | next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$} |
| 282 | r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, |
||
| 283 | :identifier => $3) |
||
| 284 | blame.add_line($4.rstrip, r)
|
||
| 285 | 0:513646585e45 | Chris | end
|
| 286 | end
|
||
| 287 | blame |
||
| 288 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 289 | 507:0c939c159af4 | Chris | # means not found or cannot be annotated
|
| 290 | Annotate.new
|
||
| 291 | 0:513646585e45 | Chris | end
|
| 292 | 119:8661b858af72 | Chris | |
| 293 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 294 | # Returns the readable identifier
|
||
| 295 | def format_identifier |
||
| 296 | "#{revision}:#{scmid}"
|
||
| 297 | end
|
||
| 298 | end
|
||
| 299 | |||
| 300 | 245:051f544170fe | Chris | # Runs 'hg' command with the given args
|
| 301 | def hg(*args, &block) |
||
| 302 | repo_path = root_url || url |
||
| 303 | 909:cbb26bc654de | Chris | full_args = ['-R', repo_path, '--encoding', 'utf-8'] |
| 304 | 245:051f544170fe | Chris | full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" |
| 305 | full_args << '--config' << 'diff.git=false' |
||
| 306 | full_args += args |
||
| 307 | 909:cbb26bc654de | Chris | ret = shellout( |
| 308 | self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '), |
||
| 309 | &block |
||
| 310 | ) |
||
| 311 | 245:051f544170fe | Chris | if $? && $?.exitstatus != 0 |
| 312 | raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" |
||
| 313 | end
|
||
| 314 | ret |
||
| 315 | end
|
||
| 316 | private :hg
|
||
| 317 | |||
| 318 | 119:8661b858af72 | Chris | # Returns correct revision identifier
|
| 319 | 245:051f544170fe | Chris | def hgrev(identifier, sq=false) |
| 320 | rev = identifier.blank? ? 'tip' : identifier.to_s
|
||
| 321 | rev = shell_quote(rev) if sq
|
||
| 322 | rev |
||
| 323 | 119:8661b858af72 | Chris | end
|
| 324 | private :hgrev
|
||
| 325 | 245:051f544170fe | Chris | |
| 326 | def hgtarget(path) |
||
| 327 | path ||= ''
|
||
| 328 | root_url + '/' + without_leading_slash(path)
|
||
| 329 | end
|
||
| 330 | private :hgtarget
|
||
| 331 | |||
| 332 | def as_ary(o) |
||
| 333 | return [] unless o |
||
| 334 | o.is_a?(Array) ? o : Array[o] |
||
| 335 | end
|
||
| 336 | private :as_ary
|
||
| 337 | 0:513646585e45 | Chris | end
|
| 338 | end
|
||
| 339 | end
|
||
| 340 | end |