annotate lib/redmine/scm/adapters/mercurial_adapter.rb @ 879:8fb3bed996c3 feature_115

Close obsolete branch feature_115
author Chris Cannam
date Sat, 26 Mar 2011 15:07:00 +0000
parents e297bf495063
children de76cd3e8c8e b859cc0c4fa1
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@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