annotate lib/redmine/scm/adapters/mercurial_adapter.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents ed5f37ea0d06
children e297bf495063
rev   line source
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@6 144 doc = hg(*hg_args) { |io| REXML::Document.new(io.read.concat('</log>')) }
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