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 / .svn / text-base / mercurial_adapter.rb.svn-base @ 441:cbce1fd3b1b7
History | View | Annotate | Download (11.5 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 | @@sq_bin ||= shell_quote(HG_BIN) |
||
| 43 | 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 | 441:cbce1fd3b1b7 | Chris | client_version_above?([0, 9, 5]) |
| 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 | if ((version <=> [0,9,5]) > 0) || version.empty? |
||
| 76 | ver = "1.0" |
||
| 77 | else |
||
| 78 | ver = "0.9.5" |
||
| 79 | end |
||
| 80 | 245:051f544170fe | Chris | "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
|
| 81 | 0:513646585e45 | Chris | end |
| 82 | end |
||
| 83 | 119:8661b858af72 | Chris | |
| 84 | 245:051f544170fe | Chris | def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) |
| 85 | super |
||
| 86 | 441:cbce1fd3b1b7 | Chris | @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding |
| 87 | end |
||
| 88 | |||
| 89 | def path_encoding |
||
| 90 | @path_encoding |
||
| 91 | 0:513646585e45 | Chris | end |
| 92 | 119:8661b858af72 | Chris | |
| 93 | 245:051f544170fe | Chris | def info |
| 94 | tip = summary['repository']['tip'] |
||
| 95 | Info.new(:root_url => CGI.unescape(summary['repository']['root']), |
||
| 96 | :lastrev => Revision.new(:revision => tip['revision'], |
||
| 97 | :scmid => tip['node'])) |
||
| 98 | end |
||
| 99 | |||
| 100 | def tags |
||
| 101 | as_ary(summary['repository']['tag']).map { |e| e['name'] }
|
||
| 102 | end |
||
| 103 | |||
| 104 | # Returns map of {'tag' => 'nodeid', ...}
|
||
| 105 | def tagmap |
||
| 106 | alist = as_ary(summary['repository']['tag']).map do |e| |
||
| 107 | e.values_at('name', 'node')
|
||
| 108 | end |
||
| 109 | Hash[*alist.flatten] |
||
| 110 | end |
||
| 111 | |||
| 112 | def branches |
||
| 113 | as_ary(summary['repository']['branch']).map { |e| e['name'] }
|
||
| 114 | end |
||
| 115 | |||
| 116 | # Returns map of {'branch' => 'nodeid', ...}
|
||
| 117 | def branchmap |
||
| 118 | alist = as_ary(summary['repository']['branch']).map do |e| |
||
| 119 | e.values_at('name', 'node')
|
||
| 120 | end |
||
| 121 | Hash[*alist.flatten] |
||
| 122 | end |
||
| 123 | |||
| 124 | def summary |
||
| 125 | 441:cbce1fd3b1b7 | Chris | return @summary if @summary |
| 126 | 245:051f544170fe | Chris | hg 'rhsummary' do |io| |
| 127 | output = io.read |
||
| 128 | if output.respond_to?(:force_encoding) |
||
| 129 | output.force_encoding('UTF-8')
|
||
| 130 | end |
||
| 131 | begin |
||
| 132 | @summary = ActiveSupport::XmlMini.parse(output)['rhsummary'] |
||
| 133 | rescue |
||
| 134 | 0:513646585e45 | Chris | end |
| 135 | end |
||
| 136 | 245:051f544170fe | Chris | end |
| 137 | private :summary |
||
| 138 | |||
| 139 | 441:cbce1fd3b1b7 | Chris | def entries(path=nil, identifier=nil, options={})
|
| 140 | 245:051f544170fe | Chris | p1 = scm_iconv(@path_encoding, 'UTF-8', path) |
| 141 | manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
|
||
| 142 | CGI.escape(without_leading_slash(p1.to_s))) do |io| |
||
| 143 | output = io.read |
||
| 144 | if output.respond_to?(:force_encoding) |
||
| 145 | output.force_encoding('UTF-8')
|
||
| 146 | end |
||
| 147 | begin |
||
| 148 | ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest'] |
||
| 149 | rescue |
||
| 150 | end |
||
| 151 | end |
||
| 152 | path_prefix = path.blank? ? '' : with_trailling_slash(path) |
||
| 153 | |||
| 154 | entries = Entries.new |
||
| 155 | as_ary(manifest['dir']).each do |e| |
||
| 156 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
|
||
| 157 | p = "#{path_prefix}#{n}"
|
||
| 158 | entries << Entry.new(:name => n, :path => p, :kind => 'dir') |
||
| 159 | end |
||
| 160 | |||
| 161 | as_ary(manifest['file']).each do |e| |
||
| 162 | n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
|
||
| 163 | p = "#{path_prefix}#{n}"
|
||
| 164 | lr = Revision.new(:revision => e['revision'], :scmid => e['node'], |
||
| 165 | :identifier => e['node'], |
||
| 166 | :time => Time.at(e['time'].to_i)) |
||
| 167 | entries << Entry.new(:name => n, :path => p, :kind => 'file', |
||
| 168 | :size => e['size'].to_i, :lastrev => lr) |
||
| 169 | end |
||
| 170 | |||
| 171 | entries |
||
| 172 | rescue HgCommandAborted |
||
| 173 | nil # means not found |
||
| 174 | 0:513646585e45 | Chris | end |
| 175 | 119:8661b858af72 | Chris | |
| 176 | 245:051f544170fe | Chris | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 177 | revs = Revisions.new |
||
| 178 | each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
|
||
| 179 | revs |
||
| 180 | end |
||
| 181 | |||
| 182 | # Iterates the revisions by using a template file that |
||
| 183 | 0:513646585e45 | Chris | # makes Mercurial produce a xml output. |
| 184 | 245:051f544170fe | Chris | def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
| 185 | hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] |
||
| 186 | hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
|
||
| 187 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 188 | hg_args << hgtarget(path) unless path.blank? |
||
| 189 | log = hg(*hg_args) do |io| |
||
| 190 | output = io.read |
||
| 191 | if output.respond_to?(:force_encoding) |
||
| 192 | output.force_encoding('UTF-8')
|
||
| 193 | end |
||
| 194 | 0:513646585e45 | Chris | begin |
| 195 | 245:051f544170fe | Chris | # Mercurial < 1.5 does not support footer template for '</log>' |
| 196 | ActiveSupport::XmlMini.parse("#{output}</log>")['log']
|
||
| 197 | 0:513646585e45 | Chris | rescue |
| 198 | end |
||
| 199 | end |
||
| 200 | 245:051f544170fe | Chris | as_ary(log['logentry']).each do |le| |
| 201 | cpalist = as_ary(le['paths']['path-copied']).map do |e| |
||
| 202 | 441:cbce1fd3b1b7 | Chris | [e['__content__'], e['copyfrom-path']].map do |s| |
| 203 | scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
|
||
| 204 | end |
||
| 205 | 245:051f544170fe | Chris | end |
| 206 | cpmap = Hash[*cpalist.flatten] |
||
| 207 | paths = as_ary(le['paths']['path']).map do |e| |
||
| 208 | p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
|
||
| 209 | 441:cbce1fd3b1b7 | Chris | {:action => e['action'],
|
| 210 | :path => with_leading_slash(p), |
||
| 211 | :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), |
||
| 212 | :from_revision => (cpmap.member?(p) ? le['node'] : nil)} |
||
| 213 | 245:051f544170fe | Chris | end.sort { |a, b| a[:path] <=> b[:path] }
|
| 214 | yield Revision.new(:revision => le['revision'], |
||
| 215 | 441:cbce1fd3b1b7 | Chris | :scmid => le['node'], |
| 216 | :author => (le['author']['__content__'] rescue ''), |
||
| 217 | :time => Time.parse(le['date']['__content__']), |
||
| 218 | :message => le['msg']['__content__'], |
||
| 219 | :paths => paths) |
||
| 220 | 245:051f544170fe | Chris | end |
| 221 | self |
||
| 222 | 0:513646585e45 | Chris | end |
| 223 | 119:8661b858af72 | Chris | |
| 224 | 441:cbce1fd3b1b7 | Chris | # Returns list of nodes in the specified branch |
| 225 | def nodes_in_branch(branch, options={})
|
||
| 226 | hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
|
||
| 227 | hg_args << '--from' << CGI.escape(branch) |
||
| 228 | hg_args << '--to' << '0' |
||
| 229 | hg_args << '--limit' << options[:limit] if options[:limit] |
||
| 230 | hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
|
||
| 231 | end |
||
| 232 | |||
| 233 | 0:513646585e45 | Chris | def diff(path, identifier_from, identifier_to=nil) |
| 234 | 245:051f544170fe | Chris | hg_args = %w|rhdiff| |
| 235 | if identifier_to |
||
| 236 | hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) |
||
| 237 | else |
||
| 238 | hg_args << '-c' << hgrev(identifier_from) |
||
| 239 | end |
||
| 240 | unless path.blank? |
||
| 241 | p = scm_iconv(@path_encoding, 'UTF-8', path) |
||
| 242 | hg_args << CGI.escape(hgtarget(p)) |
||
| 243 | end |
||
| 244 | 119:8661b858af72 | Chris | diff = [] |
| 245 | 245:051f544170fe | Chris | hg *hg_args do |io| |
| 246 | 0:513646585e45 | Chris | io.each_line do |line| |
| 247 | diff << line |
||
| 248 | end |
||
| 249 | end |
||
| 250 | diff |
||
| 251 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 252 | nil # means not found |
||
| 253 | 0:513646585e45 | Chris | end |
| 254 | 119:8661b858af72 | Chris | |
| 255 | 0:513646585e45 | Chris | def cat(path, identifier=nil) |
| 256 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 257 | 441:cbce1fd3b1b7 | Chris | hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 258 | 0:513646585e45 | Chris | io.binmode |
| 259 | 245:051f544170fe | Chris | io.read |
| 260 | 0:513646585e45 | Chris | end |
| 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 annotate(path, identifier=nil) |
| 266 | 245:051f544170fe | Chris | p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) |
| 267 | 0:513646585e45 | Chris | blame = Annotate.new |
| 268 | 441:cbce1fd3b1b7 | Chris | hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| |
| 269 | 0:513646585e45 | Chris | io.each_line do |line| |
| 270 | 245:051f544170fe | Chris | line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
|
| 271 | 119:8661b858af72 | Chris | next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
|
| 272 | r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, |
||
| 273 | :identifier => $3) |
||
| 274 | blame.add_line($4.rstrip, r) |
||
| 275 | 0:513646585e45 | Chris | end |
| 276 | end |
||
| 277 | blame |
||
| 278 | 245:051f544170fe | Chris | rescue HgCommandAborted |
| 279 | nil # means not found or cannot be annotated |
||
| 280 | 0:513646585e45 | Chris | end |
| 281 | 119:8661b858af72 | Chris | |
| 282 | class Revision < Redmine::Scm::Adapters::Revision |
||
| 283 | # Returns the readable identifier |
||
| 284 | def format_identifier |
||
| 285 | "#{revision}:#{scmid}"
|
||
| 286 | end |
||
| 287 | end |
||
| 288 | |||
| 289 | 245:051f544170fe | Chris | # Runs 'hg' command with the given args |
| 290 | def hg(*args, &block) |
||
| 291 | repo_path = root_url || url |
||
| 292 | full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8'] |
||
| 293 | full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
|
||
| 294 | full_args << '--config' << 'diff.git=false' |
||
| 295 | full_args += args |
||
| 296 | ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
|
||
| 297 | if $? && $?.exitstatus != 0 |
||
| 298 | raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
|
||
| 299 | end |
||
| 300 | ret |
||
| 301 | end |
||
| 302 | private :hg |
||
| 303 | |||
| 304 | 119:8661b858af72 | Chris | # Returns correct revision identifier |
| 305 | 245:051f544170fe | Chris | def hgrev(identifier, sq=false) |
| 306 | rev = identifier.blank? ? 'tip' : identifier.to_s |
||
| 307 | rev = shell_quote(rev) if sq |
||
| 308 | rev |
||
| 309 | 119:8661b858af72 | Chris | end |
| 310 | private :hgrev |
||
| 311 | 245:051f544170fe | Chris | |
| 312 | def hgtarget(path) |
||
| 313 | path ||= '' |
||
| 314 | root_url + '/' + without_leading_slash(path) |
||
| 315 | end |
||
| 316 | private :hgtarget |
||
| 317 | |||
| 318 | def as_ary(o) |
||
| 319 | return [] unless o |
||
| 320 | o.is_a?(Array) ? o : Array[o] |
||
| 321 | end |
||
| 322 | private :as_ary |
||
| 323 | 0:513646585e45 | Chris | end |
| 324 | end |
||
| 325 | end |
||
| 326 | end |