Feature #4455 » redmine-mercurial.patch
| app/models/repository/mercurial.rb | ||
|---|---|---|
| 28 | 28 | def self.scm_name | 
| 29 | 29 | 'Mercurial' | 
| 30 | 30 | end | 
| 31 |  | |
| 32 | def entries(path=nil, identifier=nil) | |
| 33 | entries=scm.entries(path, identifier) | |
| 34 | if entries | |
| 35 | entries.each do |entry| | |
| 36 | next unless entry.is_file? | |
| 37 | # Set the filesize unless browsing a specific revision | |
| 38 | if identifier.nil? | |
| 39 | full_path = File.join(root_url, entry.path) | |
| 40 | entry.size = File.stat(full_path).size if File.file?(full_path) | |
| 41 | end | |
| 42 | # Search the DB for the entry's last change | |
| 43 |         change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") | |
| 44 | if change | |
| 45 | entry.lastrev.identifier = change.changeset.revision | |
| 46 | entry.lastrev.name = change.changeset.revision | |
| 47 | entry.lastrev.author = change.changeset.committer | |
| 48 | entry.lastrev.revision = change.revision | |
| 49 | end | |
| 50 | end | |
| 51 | end | |
| 52 | entries | |
| 31 | ||
| 32 | def branches | |
| 33 | scm.branches | |
| 34 | end | |
| 35 | ||
| 36 | def tags | |
| 37 | scm.tags | |
| 53 | 38 | end | 
| 54 | 39 | |
| 40 | # Sequential changesets are brittle in Mercurial, so we take | |
| 41 | # a leaf out of Git's book, but run two passes to take | |
| 42 | # advantage of the 'lite' log speed to build our sync list | |
| 55 | 43 | def fetch_changesets | 
| 56 | 44 | scm_info = scm.info | 
| 57 | if scm_info | |
| 58 | # latest revision found in database | |
| 59 | db_revision = latest_changeset ? latest_changeset.revision.to_i : -1 | |
| 60 | # latest revision in the repository | |
| 61 | latest_revision = scm_info.lastrev | |
| 62 | return if latest_revision.nil? | |
| 63 | scm_revision = latest_revision.identifier.to_i | |
| 64 | if db_revision < scm_revision | |
| 65 |         logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? | |
| 66 | identifier_from = db_revision + 1 | |
| 67 | while (identifier_from <= scm_revision) | |
| 68 | # loads changesets by batches of 100 | |
| 69 | identifier_to = [identifier_from + 99, scm_revision].min | |
| 70 |           revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true) | |
| 71 | transaction do | |
| 72 | revisions.each do |revision| | |
| 73 | changeset = Changeset.create(:repository => self, | |
| 74 | :revision => revision.identifier, | |
| 75 | :scmid => revision.scmid, | |
| 76 | :committer => revision.author, | |
| 77 | :committed_on => revision.time, | |
| 78 | :comments => revision.message) | |
| 79 |  | |
| 80 | revision.paths.each do |change| | |
| 81 | Change.create(:changeset => changeset, | |
| 82 | :action => change[:action], | |
| 83 | :path => change[:path], | |
| 84 | :from_path => change[:from_path], | |
| 85 | :from_revision => change[:from_revision]) | |
| 86 | end | |
| 87 | end | |
| 88 | end unless revisions.nil? | |
| 89 | identifier_from = identifier_to + 1 | |
| 90 | end | |
| 91 | end | |
| 92 | end | |
| 45 | return unless scm_info or scm_info.lastrev.nil? | |
| 46 |  | |
| 47 | db_revision = latest_changeset ? latest_changeset.scmid.to_s : 0 | |
| 48 | scm_revision = scm_info.lastrev.scmid.to_s | |
| 49 | # Save ourselves an expensive operation if we're already up to date | |
| 50 | scm_revcount = scm.num_revisions | |
| 51 | db_revcount = changesets.count | |
| 52 | return if scm.num_revisions == changesets.count and db_revision == scm_revision | |
| 53 |  | |
| 54 | lite_revisions = scm.revisions(nil, nil, scm_revision, :lite => true) | |
| 55 | return if lite_revisions.nil? or lite_revisions.empty? | |
| 56 | ||
| 57 | # Find revisions that redmine knows about already | |
| 58 |     existing_revisions = changesets.find(:all).map!{|c| c.scmid} | |
| 59 | ||
| 60 | # Clean out revisions that are no longer in Mercurial | |
| 61 |     Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", lite_revisions.map{|r| r.scmid}, self.id]) | |
| 62 | ||
| 63 | # Subtract revisions that redmine already knows about | |
| 64 |     lite_revisions.reject!{|r| existing_revisions.include?(r.scmid)} | |
| 65 | return if lite_revisions.nil? or lite_revisions.empty? | |
| 66 |  | |
| 67 | # Retrieve full revisions for the remainder | |
| 68 | revisions = [] | |
| 69 |     lite_revisions.each {|r| revisions += scm.revisions(nil, r.scmid, r.scmid)} | |
| 70 | return if revisions.nil? or revisions.empty? | |
| 71 | ||
| 72 | # Save the results to the database | |
| 73 |     revisions.each{|r| r.save(self)} unless revisions.nil? | |
| 74 | end | |
| 75 |  | |
| 76 | def latest_changesets(path, rev, limit=10) | |
| 77 | revisions = scm.revisions(path, rev, 0, :limit => limit, :lite => true) | |
| 78 | return [] if revisions.nil? or revisions.empty? | |
| 79 | ||
| 80 | changesets.find( | |
| 81 | :all, | |
| 82 | :conditions => [ | |
| 83 | "scmid IN (?)", | |
| 84 |         revisions.map!{|c| c.scmid} | |
| 85 | ], | |
| 86 | :order => 'committed_on DESC' | |
| 87 | ) | |
| 93 | 88 | end | 
| 94 | 89 | end | 
| lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl | ||
|---|---|---|
| 1 | changeset = 'This template must be used with --debug option\n' | |
| 2 | changeset_quiet = 'This template must be used with --debug option\n' | |
| 3 | changeset_verbose = 'This template must be used with --debug option\n' | |
| 4 | changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 5 | ||
| 6 | tag = '<tag>{tag|escape}</tag>\n' | |
| 7 | header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' | |
| 8 | # footer="</log>" | |
| lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl | ||
|---|---|---|
| 1 | 1 | changeset = 'This template must be used with --debug option\n' | 
| 2 | 2 | changeset_quiet = 'This template must be used with --debug option\n' | 
| 3 | 3 | changeset_verbose = 'This template must be used with --debug option\n' | 
| 4 | changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 4 | changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 5 | 5 | |
| 6 | 6 | file = '<path action="M">{file|escape}</path>\n' | 
| 7 | 7 | file_add = '<path action="A">{file_add|escape}</path>\n' | 
| lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl | ||
|---|---|---|
| 1 | changeset = 'This template must be used with --debug option\n' | |
| 2 | changeset_quiet = 'This template must be used with --debug option\n' | |
| 3 | changeset_verbose = 'This template must be used with --debug option\n' | |
| 4 | changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths />\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 5 | ||
| 6 | tag = '<tag>{tag|escape}</tag>\n' | |
| 7 | header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' | |
| 8 | # footer="</log>" | |
| lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl | ||
|---|---|---|
| 1 | 1 | changeset = 'This template must be used with --debug option\n' | 
| 2 | 2 | changeset_quiet = 'This template must be used with --debug option\n' | 
| 3 | 3 | changeset_verbose = 'This template must be used with --debug option\n' | 
| 4 | changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 4 | changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' | |
| 5 | 5 | |
| 6 | 6 | file_mod = '<path action="M">{file_mod|escape}</path>\n' | 
| 7 | 7 | file_add = '<path action="A">{file_add|escape}</path>\n' | 
| lib/redmine/scm/adapters/mercurial_adapter.rb | ||
|---|---|---|
| 21 | 21 | module Scm | 
| 22 | 22 | module Adapters | 
| 23 | 23 | class MercurialAdapter < AbstractAdapter | 
| 24 |  | |
| 24 | ||
| 25 | 25 | # Mercurial executable name | 
| 26 | 26 | HG_BIN = "hg" | 
| 27 | 27 | TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" | 
| 28 | 28 | TEMPLATE_NAME = "hg-template" | 
| 29 | 29 | TEMPLATE_EXTENSION = "tmpl" | 
| 30 |  | |
| 30 | ||
| 31 | 31 | class << self | 
| 32 | 32 | def client_version | 
| 33 | 33 | @@client_version ||= (hgversion || []) | 
| 34 | 34 | end | 
| 35 |  | |
| 35 | ||
| 36 | 36 | def hgversion | 
| 37 | 37 | # The hg version is expressed either as a | 
| 38 | 38 | # release number (eg 0.9.5 or 1.0) or as a revision | 
| ... | ... | |
| 42 | 42 |               theversion.split(".").collect(&:to_i) | 
| 43 | 43 | end | 
| 44 | 44 | end | 
| 45 |  | |
| 45 | ||
| 46 | 46 | def hgversion_from_command_line | 
| 47 | 47 |             %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] | 
| 48 | 48 | end | 
| 49 |  | |
| 49 | ||
| 50 | 50 | def template_path | 
| 51 | 51 | @@template_path ||= template_path_for(client_version) | 
| 52 | 52 | end | 
| 53 |  | |
| 54 | def template_path_for(version) | |
| 53 | ||
| 54 | def lite_template_path | |
| 55 | @@lite_template_path ||= template_path_for(client_version,'lite') | |
| 56 | end | |
| 57 | ||
| 58 | def template_path_for(version,style=nil) | |
| 55 | 59 | if ((version <=> [0,9,5]) > 0) || version.empty? | 
| 56 | 60 | ver = "1.0" | 
| 57 | 61 | else | 
| 58 | 62 | ver = "0.9.5" | 
| 59 | 63 | end | 
| 60 |             "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | |
| 64 | if style | |
| 65 |               tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}" | |
| 66 | else | |
| 67 |             	tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | |
| 68 | end | |
| 69 | tmpl | |
| 70 | end | |
| 71 | end | |
| 72 | ||
| 73 | def default_branch | |
| 74 | @default_branch ||= 'tip' | |
| 75 | end | |
| 76 | ||
| 77 | def branches | |
| 78 | @branches ||= get_branches | |
| 79 | end | |
| 80 | ||
| 81 | def get_branches | |
| 82 | branches = [] | |
| 83 |           cmd = "#{HG_BIN} -R #{target('')} branches" | |
| 84 | shellout(cmd) do |io| | |
| 85 | io.each_line do |line| | |
| 86 |               branches << line.chomp.match('^([^\s]+).*$')[1] | |
| 87 | end | |
| 88 | end | |
| 89 | branches.sort! | |
| 90 | end | |
| 91 | ||
| 92 | def tags | |
| 93 | @tags ||= get_tags | |
| 94 | end | |
| 95 | ||
| 96 | def get_tags | |
| 97 | tags = [] | |
| 98 |           cmd = "#{HG_BIN} -R #{target('')} tags" | |
| 99 | shellout(cmd) do |io| | |
| 100 | io.each_line do |line| | |
| 101 |               tags << line.chomp.match('^([\w]+).*$')[1] | |
| 102 | end | |
| 61 | 103 | end | 
| 104 | tags.sort! | |
| 105 | end | |
| 106 | ||
| 107 | def tip | |
| 108 | @tip ||= get_tip | |
| 62 | 109 | end | 
| 63 | 110 |  | 
| 111 | def get_tip | |
| 112 | tip = nil | |
| 113 |           cmd = "#{HG_BIN} -R #{target('')} tip" | |
| 114 | shellout(cmd) do |io| | |
| 115 |             tip = io.gets.chomp.match('^changeset:\s+\d+:(\w+)$')[1] | |
| 116 | end | |
| 117 | return nil if $? && $?.exitstatus != 0 | |
| 118 | tip | |
| 119 | end | |
| 120 | ||
| 64 | 121 | def info | 
| 65 | 122 |           cmd = "#{HG_BIN} -R #{target('')} root" | 
| 66 | 123 | root_url = nil | 
| ... | ... | |
| 69 | 126 | end | 
| 70 | 127 | return nil if $? && $?.exitstatus != 0 | 
| 71 | 128 |           info = Info.new({:root_url => root_url.chomp, | 
| 72 |                             :lastrev => revisions(nil,nil,nil,{:limit => 1}).last | |
| 129 |                             :lastrev => lastrev(nil,tip) | |
| 73 | 130 | }) | 
| 74 | 131 | info | 
| 75 | 132 | rescue CommandFailed | 
| 76 | 133 | return nil | 
| 77 | 134 | end | 
| 78 | 135 |  | 
| 79 | def entries(path=nil, identifier=nil) | |
| 136 | def lastrev(path=nil, identifier=nil) | |
| 137 | lastrev = revisions(path,identifier,0,:limit => 1, :lite => true) | |
| 138 | return nil if lastrev.nil? or lastrev.empty? | |
| 139 | lastrev.last | |
| 140 | end | |
| 141 | ||
| 142 | def num_revisions | |
| 143 |           cmd = "#{HG_BIN} -R #{target('')} log -r :tip --template='\n' | wc -l" | |
| 144 |           shellout(cmd) {|io| io.gets.chomp.to_i} | |
| 145 | end | |
| 146 |  | |
| 147 | # Returns the entry identified by path and revision identifier | |
| 148 | # or nil if entry doesn't exist in the repository | |
| 149 | def entry(path=nil, identifier=nil) | |
| 150 |           parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?} | |
| 151 |           search_path = parts[0..-2].join('/') | |
| 152 | search_name = parts[-1] | |
| 153 | if search_path.blank? && search_name.blank? | |
| 154 | # Root entry | |
| 155 | Entry.new(:path => '', :kind => 'dir') | |
| 156 | else | |
| 157 | # Search for the entry in the parent directory | |
| 158 | es = entries(search_path, identifier, :search => search_name) | |
| 159 |             es ? es.detect {|e| e.name == search_name} : nil | |
| 160 | end | |
| 161 | end | |
| 162 | ||
| 163 |         def entries(path=nil, identifier=nil, options={})   | |
| 80 | 164 | path ||= '' | 
| 165 | identifier ||= 'tip' | |
| 81 | 166 | entries = Entries.new | 
| 82 | 167 |           cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" | 
| 83 | cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
| 168 |           cmd << " -r #{shell_quote(identifier.to_s)}" | |
| 169 | cmd << " -I" if options[:search] unless path.empty? | |
| 84 | 170 |           cmd << " " + shell_quote("path:#{path}") unless path.empty? | 
| 171 | cmd << " " + shell_quote(options[:search]) if options[:search] | |
| 85 | 172 | shellout(cmd) do |io| | 
| 86 | 173 | io.each_line do |line| | 
| 87 | 174 | # HG uses antislashs as separator on Windows | 
| ... | ... | |
| 89 | 176 |               if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') | 
| 90 | 177 | e ||= line | 
| 91 | 178 |                 e = e.chomp.split(%r{[\/\\]}) | 
| 179 | k = (e.size > 1 ? 'dir' : 'file') | |
| 180 |                 p = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}") | |
| 181 | # Always set the file size if we have the 'size' extension for | |
| 182 | # Mercurial, otherwise set from the filesystem if we're browsing | |
| 183 | # the default 'branch' (tip) | |
| 184 | s = nil | |
| 185 | if (k == 'file') | |
| 186 | s = size(p,identifier) | |
| 187 | if s.nil? and (identifier.to_s == default_branch or identifier.to_s == 'tip') | |
| 188 | full_path = info.root_url + '/' + p | |
| 189 | s = File.stat(full_path).size if File.file?(full_path) | |
| 190 | end | |
| 191 | end | |
| 92 | 192 |                 entries << Entry.new({:name => e.first, | 
| 93 |                                        :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), | |
| 94 | :kind => (e.size > 1 ? 'dir' : 'file'), | |
| 95 | :lastrev => Revision.new | |
| 193 | :path => p, | |
| 194 | :kind => k, | |
| 195 | :size => s, | |
| 196 | :lastrev => lastrev(p,identifier) | |
| 96 | 197 |                                      }) unless e.empty? || entries.detect{|entry| entry.name == e.first} | 
| 97 | 198 | end | 
| 98 | 199 | end | 
| ... | ... | |
| 100 | 201 | return nil if $? && $?.exitstatus != 0 | 
| 101 | 202 | entries.sort_by_name | 
| 102 | 203 | end | 
| 103 |  | |
| 204 | ||
| 104 | 205 | # Fetch the revisions by using a template file that | 
| 105 | 206 | # makes Mercurial produce a xml output. | 
| 106 | 207 |         def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})   | 
| 107 | 208 | revisions = Revisions.new | 
| 108 |           cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}" | |
| 209 |           cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log" | |
| 210 | if options[:lite] | |
| 211 |             cmd << " --style #{shell_quote self.class.lite_template_path}"  | |
| 212 | else | |
| 213 |           	cmd << " -C --style #{shell_quote self.class.template_path}" | |
| 214 | end | |
| 109 | 215 | if identifier_from && identifier_to | 
| 110 |             cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" | |
| 216 |             cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}" | |
| 111 | 217 | elsif identifier_from | 
| 112 |             cmd << " -r #{identifier_from.to_i}:" | |
| 218 |             cmd << " -r #{shell_quote(identifier_from.to_s)}:" | |
| 219 | elsif identifier_to | |
| 220 |             cmd << " -r :#{shell_quote(identifier_to.to_s)}" | |
| 113 | 221 | end | 
| 114 | 222 |           cmd << " --limit #{options[:limit].to_i}" if options[:limit] | 
| 115 | 223 |           cmd << " #{path}" if path | 
| 116 | 224 | shellout(cmd) do |io| | 
| 117 | 225 | begin | 
| 118 | 226 | # HG doesn't close the XML Document... | 
| 119 | doc = REXML::Document.new(io.read << "</log>") | |
| 227 | output = io.read | |
| 228 | return nil if output.empty? | |
| 229 | doc = REXML::Document.new(output << "</log>") | |
| 120 | 230 |               doc.elements.each("log/logentry") do |logentry| | 
| 121 | 231 | paths = [] | 
| 122 | 232 |                 copies = logentry.get_elements('paths/path-copied') | 
| ... | ... | |
| 124 | 234 | # Detect if the added file is a copy | 
| 125 | 235 |                   if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } | 
| 126 | 236 | from_path = c.attributes['copyfrom-path'] | 
| 127 |                     from_rev = logentry.attributes['revision'] | |
| 237 |                     from_rev = logentry.attributes['shortnode'] | |
| 128 | 238 | end | 
| 129 | 239 |                   paths << {:action => path.attributes['action'], | 
| 130 | 240 |                     :path => "/#{path.text}", | 
| ... | ... | |
| 132 | 242 | :from_revision => from_rev ? from_rev : nil | 
| 133 | 243 | } | 
| 134 | 244 | end | 
| 135 |                 paths.sort! { |x,y| x[:path] <=> y[:path] } | |
| 136 |  | |
| 137 |                 revisions << Revision.new({:identifier => logentry.attributes['revision'], | |
| 245 |                 paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty? | |
| 246 | ||
| 247 |                 revisions << Revision.new({:identifier => logentry.attributes['shortnode'], | |
| 138 | 248 | :scmid => logentry.attributes['node'], | 
| 139 | 249 | :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), | 
| 140 | 250 | :time => Time.parse(logentry.elements['date'].text).localtime, | 
| ... | ... | |
| 149 | 259 | return nil if $? && $?.exitstatus != 0 | 
| 150 | 260 | revisions | 
| 151 | 261 | end | 
| 152 |  | |
| 262 | ||
| 153 | 263 | def diff(path, identifier_from, identifier_to=nil) | 
| 154 | 264 | path ||= '' | 
| 155 | 265 | if identifier_to | 
| 156 |             identifier_to = identifier_to.to_i  | |
| 266 |             cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates" | |
| 157 | 267 | else | 
| 158 |             identifier_to = identifier_from.to_i - 1 | |
| 268 |             cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates" | |
| 159 | 269 | end | 
| 160 |           cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates" | |
| 161 | 270 |           cmd << " -I #{target(path)}" unless path.empty? | 
| 162 | 271 | diff = [] | 
| 163 | 272 | shellout(cmd) do |io| | 
| ... | ... | |
| 168 | 277 | return nil if $? && $?.exitstatus != 0 | 
| 169 | 278 | diff | 
| 170 | 279 | end | 
| 171 |  | |
| 280 | ||
| 172 | 281 | def cat(path, identifier=nil) | 
| 173 | 282 |           cmd = "#{HG_BIN} -R #{target('')} cat" | 
| 174 |           cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
| 175 |           cmd << " #{target(path)}" | |
| 283 |           cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) | |
| 284 |           cmd << " #{target(path)}" unless path.empty? | |
| 176 | 285 | cat = nil | 
| 177 | 286 | shellout(cmd) do |io| | 
| 178 | 287 | io.binmode | 
| ... | ... | |
| 181 | 290 | return nil if $? && $?.exitstatus != 0 | 
| 182 | 291 | cat | 
| 183 | 292 | end | 
| 184 |  | |
| 293 | ||
| 294 | def size(path, identifier=nil) | |
| 295 |           cmd = "#{HG_BIN} --cwd #{target('')} size" | |
| 296 | cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) | |
| 297 |           cmd << " #{path}" unless path.empty? | |
| 298 | size = nil | |
| 299 | shellout(cmd) do |io| | |
| 300 | size = io.read | |
| 301 | end | |
| 302 | return nil if $? && $?.exitstatus != 0 | |
| 303 | size.to_i | |
| 304 | end | |
| 305 | ||
| 185 | 306 | def annotate(path, identifier=nil) | 
| 186 | 307 | path ||= '' | 
| 187 | 308 |           cmd = "#{HG_BIN} -R #{target('')}" | 
| 188 | cmd << " annotate -n -u" | |
| 189 | cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
| 190 |           cmd << " -r #{identifier.to_i}" if identifier | |
| 191 |           cmd << " #{target(path)}" | |
| 309 | cmd << " annotate -c -u" | |
| 310 |           cmd << " -r #{shell_quote(identifier.to_s)}" if identifier | |
| 311 |           cmd << " #{target(path)}" unless path.empty? | |
| 192 | 312 | blame = Annotate.new | 
| 193 | 313 | shellout(cmd) do |io| | 
| 194 | 314 | io.each_line do |line| | 
| 195 |               next unless line =~ %r{^([^:]+)\s(\d+):(.*)$} | |
| 196 |               blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip)) | |
| 315 |               next unless line =~ %r{^([^:]+)\s(\w+):(.*)$} | |
| 316 |               blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip)) | |
| 197 | 317 | end | 
| 198 | 318 | end | 
| 199 | 319 | return nil if $? && $?.exitstatus != 0 |