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 |