subwrap 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/ProjectInfo.rb +54 -0
- data/Readme +458 -0
- data/bin/_subwrap_post_install +25 -0
- data/bin/command_completion_for_subwrap +21 -0
- data/bin/rscm_test +19 -0
- data/bin/subwrap +7 -0
- data/bin/svn +7 -0
- data/lib/subwrap.rb +6 -0
- data/lib/subwrap/subversion.rb +599 -0
- data/lib/subwrap/subversion_extensions.rb +148 -0
- data/lib/subwrap/svn_command.rb +1568 -0
- data/test/subversion_extensions_test.rb +72 -0
- data/test/subversion_test.rb +132 -0
- data/test/svn_command_test.rb +649 -0
- data/test/test_helper.rb +30 -0
- metadata +120 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'quality_extensions'
|
4
|
+
require 'quality_extensions/kernel/windows_platform'
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
if ARGV.include?('--dry-run')
|
8
|
+
include FileUtils::DryRun
|
9
|
+
else
|
10
|
+
include FileUtils::Verbose
|
11
|
+
end
|
12
|
+
|
13
|
+
if windows_platform?
|
14
|
+
#cp __FILE__, 'c:/ruby/bin'
|
15
|
+
cp 'c:/ruby/bin/subwrap.cmd', 'c:/ruby/bin/svn.cmd'
|
16
|
+
#puts Gem.cache.search('subwrap').sort_by { |g| g.version.version }.last.full_gem_path
|
17
|
+
else
|
18
|
+
bin_dir = File.expand_path(File.dirname(__FILE__))
|
19
|
+
chmod 0755, Dir["#{bin_dir}/*"]
|
20
|
+
|
21
|
+
path_command = "export PATH=`ls -dt --color=never /usr/lib/ruby/gems/1.8/gems/subwrap* | head -n1`/bin:$PATH"
|
22
|
+
system "grep gems/subwrap ~/.bash_profile || " +
|
23
|
+
"echo '#{path_command}' >> ~/.bash_profile"
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'facets/core/kernel/require_local'
|
5
|
+
require_local '../lib/subwrap/svn_command'
|
6
|
+
|
7
|
+
subcommands = (Subversion::SvnCommand.public_instance_methods - Object.methods).sort
|
8
|
+
|
9
|
+
# COMP_LINE will be something like 'svn sta' (what they started to type).
|
10
|
+
exit 0 unless /^svn\b/ =~ ENV["COMP_LINE"]
|
11
|
+
after_match = $'
|
12
|
+
|
13
|
+
# Since we only want our custom completion for the first argument passed to svn (because we want it to fall back to using default completion for all subsequent args), we need to check what the COMP_LINE is:
|
14
|
+
exit 0 if /^svn\s[\w=-]* / =~ ENV["COMP_LINE"]
|
15
|
+
# 'svn --diff-cmd=whatever ' =~ /^svn\b [\w=-]* / => 0
|
16
|
+
|
17
|
+
subcommand_match = (after_match.empty? || after_match =~ /\s$/) ? nil : after_match.split.last
|
18
|
+
subcommands = subcommands.select { |t| /^#{Regexp.escape subcommand_match}/ =~ t } if subcommand_match
|
19
|
+
|
20
|
+
puts subcommands
|
21
|
+
exit 0
|
data/bin/rscm_test
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rscm'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
#scm = RSCM::Subversion.new("http://code.qualitysmith.com/gemables")
|
8
|
+
|
9
|
+
subversion = RSCM::Subversion.new()
|
10
|
+
subversion.checkout_dir = '.'
|
11
|
+
puts "working copy revision = #{subversion.label}"
|
12
|
+
puts "url = #{subversion.repourl}"
|
13
|
+
puts "up to date? = #{subversion.uptodate?(nil)}"
|
14
|
+
|
15
|
+
|
16
|
+
#revisions = scm.revisions(Time.utc(2007, 01, 10, 12, 34, 22)) # For Subversion, you can also pass a revision number (int)
|
17
|
+
#revisions.each do |revision|
|
18
|
+
# pp revision
|
19
|
+
#end
|
data/bin/subwrap
ADDED
data/bin/svn
ADDED
data/lib/subwrap.rb
ADDED
@@ -0,0 +1,599 @@
|
|
1
|
+
# Tested by: ../../test/subversion_test.rb
|
2
|
+
$loaded ||= {}; if !$loaded[File.expand_path(__FILE__)]; $loaded[File.expand_path(__FILE__)] = true;
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rexml/document'
|
6
|
+
require 'rexml/xpath'
|
7
|
+
require 'rubygems'
|
8
|
+
|
9
|
+
gem 'facets', '>=1.8.51'
|
10
|
+
require 'facets/core/kernel/require_local'
|
11
|
+
require 'facets/core/enumerable/uniq_by'
|
12
|
+
require 'facets/core/kernel/silence_stream'
|
13
|
+
require 'facets/core/module/initializer'
|
14
|
+
|
15
|
+
gem 'quality_extensions', '>=0.0.7'
|
16
|
+
require 'quality_extensions/kernel/windows_platform'
|
17
|
+
|
18
|
+
# Had a lot of trouble getting ActiveSupport to load without giving errors! Eventually gave up on that idea since I only needed it for mattr_accessor and Facets supplies that.
|
19
|
+
#gem 'activesupport' # mattr_accessor
|
20
|
+
#require 'active_support'
|
21
|
+
#require 'active_support/core_ext/module/attribute_accessors'
|
22
|
+
#require 'facets/core/class/cattr'
|
23
|
+
gem 'quality_extensions'
|
24
|
+
require 'quality_extensions/module/attribute_accessors'
|
25
|
+
require 'quality_extensions/module/guard_method'
|
26
|
+
|
27
|
+
# RSCM is used for some of the abstraction, such as for parsing log messages into nice data structures. It seems like overkill, though, to use RSCM for most things...
|
28
|
+
gem 'rscm'
|
29
|
+
#require 'rscm'
|
30
|
+
#require 'rscm/scm/subversion'
|
31
|
+
require 'rscm/scm/subversion_log_parser'
|
32
|
+
|
33
|
+
# Wraps the Subversion shell commands for Ruby.
|
34
|
+
module Subversion
|
35
|
+
# True if you want output from svn to be colorized (useful if output is for human eyes, but not useful if using the output programatically)
|
36
|
+
@@color = false
|
37
|
+
mattr_accessor :color
|
38
|
+
mguard_method :with_color!, :@@color
|
39
|
+
|
40
|
+
# If true, will only output which command _would_ have been executed but will not actually execute it.
|
41
|
+
@@dry_run = false
|
42
|
+
mattr_accessor :dry_run
|
43
|
+
|
44
|
+
# If true, will print all commands to the screen before executing them.
|
45
|
+
@@print_commands = false
|
46
|
+
mattr_accessor :print_commands
|
47
|
+
mguard_method :print_commands!, :@@print_commands
|
48
|
+
|
49
|
+
# Adds the given items to the repository. Items may contain wildcards.
|
50
|
+
def self.add(*args)
|
51
|
+
execute "add #{args.join ' '}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets the svn:ignore property based on the given +patterns+.
|
55
|
+
# Each pattern is both the path (where the property gets set) and the property itself.
|
56
|
+
# For instance:
|
57
|
+
# "log/*.log" would add "*.log" to the svn:ignore property on the log/ directory.
|
58
|
+
# "log" would add "log" to the svn:ignore property on the ./ directory.
|
59
|
+
def self.ignore(*patterns)
|
60
|
+
|
61
|
+
patterns.each do |pattern|
|
62
|
+
path = File.dirname(pattern)
|
63
|
+
path += '/' if path == '.'
|
64
|
+
pattern = File.basename(pattern)
|
65
|
+
add_to_property 'ignore', path, pattern
|
66
|
+
end
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
def self.unignore(*patterns)
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
# Adds the given repository URL (http://svn.yourcompany.com/path/to/something) as an svn:externals.
|
74
|
+
#
|
75
|
+
# Options may include:
|
76
|
+
# * +:as+ - overrides the default behavior of naming the checkout based on the last component of the repo path
|
77
|
+
# * +:local_path+ - specifies where to set the externals property. Defaults to '.' or the dirname of +as+ if +as+ is specified
|
78
|
+
# (for example, <tt>vendor/plugins</tt> if +as+ is <tt>vendor/plugins/plugin_name</tt>).
|
79
|
+
#
|
80
|
+
def self.externalize(repo_url, options = {})
|
81
|
+
|
82
|
+
options[:as] ||= File.basename(repo_url)
|
83
|
+
#options[:as] = options[:as].ljust(29)
|
84
|
+
|
85
|
+
# You can't set the externals of './' to 'vendor/plugins/foo http://example.com/foo'
|
86
|
+
# Instead, you have to set the externals of 'vendor/plugins/' to 'foo http://example.com/foo'
|
87
|
+
# This will make that correction for you automatically.
|
88
|
+
options[:local_path] ||= File.dirname(options[:as]) # Will be '.' if options[:as] has no dirname component.
|
89
|
+
# Will be 'vendor/plugins' if options[:as] is 'vendor/plugins/plugin_name'.
|
90
|
+
options[:as] = File.basename(options[:as])
|
91
|
+
|
92
|
+
add_to_property 'externals', options[:local_path], "#{options[:as]} #{repo_url}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.export(path_or_url, target)
|
96
|
+
execute "export #{path_or_url} #{target}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Removes the given items from the repository and the disk. Items may contain wildcards.
|
100
|
+
def self.remove(*args)
|
101
|
+
execute "rm #{args.join ' '}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Removes the given items from the repository and the disk. Items may contain wildcards.
|
105
|
+
# To do: add a :force => true option to remove
|
106
|
+
def self.remove_force(*args)
|
107
|
+
execute "rm --force #{args.join ' '}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Removes the given items from the repository BUT NOT THE DISK. Items may contain wildcards.
|
111
|
+
def self.remove_without_delete(*args)
|
112
|
+
# resolve the wildcards before iterating
|
113
|
+
args.collect {|path| Dir[path]}.flatten.each do |path|
|
114
|
+
entries_file = "#{File.dirname(path)}/.svn/entries"
|
115
|
+
File.chmod(0644, entries_file)
|
116
|
+
|
117
|
+
xmldoc = REXML::Document.new(IO.read(entries_file))
|
118
|
+
# first attempt to delete a matching entry with schedule == add
|
119
|
+
unless xmldoc.root.elements.delete "//entry[@name='#{File.basename(path)}'][@schedule='add']"
|
120
|
+
# then attempt to alter a missing schedule to schedule=delete
|
121
|
+
entry = REXML::XPath.first(xmldoc, "//entry[@name='#{File.basename(path)}']")
|
122
|
+
entry.attributes['schedule'] ||= 'delete' if entry
|
123
|
+
end
|
124
|
+
# write back to the file
|
125
|
+
File.open(entries_file, 'w') { |f| xmldoc.write f, 0 }
|
126
|
+
|
127
|
+
File.chmod(0444, entries_file)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Reverts the given items in the working copy. Items may contain wildcards.
|
132
|
+
def self.revert(*args)
|
133
|
+
execute "revert #{args.join ' '}"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Marks the given items as being executable. Items may _not_ contain wildcards.
|
137
|
+
def self.make_executable(*paths)
|
138
|
+
paths.each do |path|
|
139
|
+
self.set_property 'executable', '', path
|
140
|
+
end
|
141
|
+
end
|
142
|
+
def self.make_not_executable(*paths)
|
143
|
+
paths.each do |path|
|
144
|
+
self.delete_property 'executable', path
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the status of items in the working directories +paths+. Returns the raw output from svn (use <tt>split("\n")</tt> if you want an array).
|
149
|
+
def self.status(*args)
|
150
|
+
args = ['./'] if args.empty?
|
151
|
+
execute("status #{args.join ' '}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.status_against_server(*args)
|
155
|
+
args = ['./'] if args.empty?
|
156
|
+
self.status('-u', *args)
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.update(*args)
|
160
|
+
args = ['./'] if args.empty?
|
161
|
+
execute("update #{args.join ' '}")
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.commit(*args)
|
165
|
+
args = ['./'] if args.empty?
|
166
|
+
execute("commit #{args.join ' '}")
|
167
|
+
end
|
168
|
+
|
169
|
+
# The output from `svn status` is nicely divided into two "sections": the section which pertains to the current working copy (not
|
170
|
+
# counting externals as part of the working copy) and then the section with status of all of the externals.
|
171
|
+
# This method returns the first section.
|
172
|
+
def self.status_the_section_before_externals(path = './')
|
173
|
+
status = status(path) || ''
|
174
|
+
status.sub!(/(Performing status.*)/m, '')
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns an array of externals *items*. These are the actual externals listed in an svn:externals property.
|
178
|
+
# Example:
|
179
|
+
# vendor/a
|
180
|
+
# vendor/b
|
181
|
+
# Where 'vendor' is an ExternalsContainer containing external items 'a' and 'b'.
|
182
|
+
def self.externals_items(path = './')
|
183
|
+
status = status_the_section_before_externals(path)
|
184
|
+
return [] if status.nil?
|
185
|
+
status.select { |line|
|
186
|
+
line =~ /^X/
|
187
|
+
}.map { |line|
|
188
|
+
# Just keep the filename part
|
189
|
+
line =~ /^X\s+(.+)/
|
190
|
+
$1
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns an array of ExternalsContainer objects representing all externals *containers* in the working directory specified by +path+.
|
195
|
+
def self.externals_containers(path = './')
|
196
|
+
# Using self.externals_items is kind of a cheap way to do this, and it results in some redundancy that we have to filter out
|
197
|
+
# (using uniq_by), but it seemed more efficient than the alternative (traversing the entire directory tree and querying for
|
198
|
+
# `svn prepget svn:externals` at each stop to see if the directory is an externals container).
|
199
|
+
self.externals_items(path).map { |external_dir|
|
200
|
+
ExternalsContainer.new(external_dir + '/..')
|
201
|
+
}.uniq_by { |external|
|
202
|
+
external.container_dir
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns the modifications to the working directory or URL specified in +args+.
|
207
|
+
def self.diff(*args)
|
208
|
+
args = ['./'] if args.empty?
|
209
|
+
execute("diff #{"--diff-cmd colordiff" if color?} #{args.join ' '}")
|
210
|
+
end
|
211
|
+
# Parses the output from diff and returns an array of Diff objects.
|
212
|
+
def self.diffs(*args)
|
213
|
+
args = ['./'] if args.empty?
|
214
|
+
raw_diffs = nil
|
215
|
+
with_color! false do
|
216
|
+
raw_diffs = diff(*args)
|
217
|
+
end
|
218
|
+
DiffsParser.new(raw_diffs).parse
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.cat(*args)
|
222
|
+
args = ['./'] if args.empty?
|
223
|
+
execute("cat #{args.join ' '}")
|
224
|
+
end
|
225
|
+
|
226
|
+
# It's easy to get/set properties, but less easy to add to a property. This method uses get/set to simulate add.
|
227
|
+
# It will uniquify lines, removing duplicates. (:todo: what if we want to set a property to have some duplicate lines?)
|
228
|
+
def self.add_to_property(property, path, *new_lines)
|
229
|
+
# :todo: I think it's possible to have properties other than svn:* ... so if property contains a prefix (something:), use it; else default to 'svn:'
|
230
|
+
|
231
|
+
# Get the current properties
|
232
|
+
lines = self.get_property(property, path).split "\n"
|
233
|
+
puts "Existing lines: #{lines.inspect}" if $debug
|
234
|
+
|
235
|
+
# Add the new lines, delete empty lines, and uniqueify all elements
|
236
|
+
lines.concat(new_lines).uniq!
|
237
|
+
puts "After concat(new_lines).uniq!: #{lines.inspect}" if $debug
|
238
|
+
|
239
|
+
lines.delete ''
|
240
|
+
# Set the property
|
241
|
+
puts "About to set propety to: #{lines.inspect}" if $debug
|
242
|
+
self.set_property property, lines.join("\n"), path
|
243
|
+
end
|
244
|
+
|
245
|
+
# :todo: Stop assuming the svn: namespace. What's the point of a namespace if you only allow one of them?
|
246
|
+
def self.get_property(property, path = './')
|
247
|
+
execute "propget svn:#{property} #{path}"
|
248
|
+
end
|
249
|
+
def self.get_revision_property(property_name, rev)
|
250
|
+
execute("propget --revprop #{property_name} -r #{rev}").chomp
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.delete_property(property, path = './')
|
254
|
+
execute "propdel svn:#{property} #{path}"
|
255
|
+
end
|
256
|
+
def self.delete_revision_property(property_name, rev)
|
257
|
+
execute("propdel --revprop #{property_name} -r #{rev}").chomp
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.set_property(property, value, path = './')
|
261
|
+
execute "propset svn:#{property} '#{value}' #{path}"
|
262
|
+
end
|
263
|
+
def self.set_revision_property(property_name, rev)
|
264
|
+
execute("propset --revprop #{property_name} -r #{rev}").chomp
|
265
|
+
end
|
266
|
+
|
267
|
+
# Gets raw output of proplist command
|
268
|
+
def self.proplist(rev)
|
269
|
+
execute("proplist --revprop -r #{rev}")
|
270
|
+
end
|
271
|
+
# Returns an array of the names of all revision properties currently set on the given +rev+
|
272
|
+
# Tessted by: ../../test/subversion_test.rb:test_revision_properties_names
|
273
|
+
def self.revision_properties_names(rev)
|
274
|
+
raw_list = proplist(rev)
|
275
|
+
raw_list.scan(/^ +([^ ]+)$/).map { |matches|
|
276
|
+
matches.first.chomp
|
277
|
+
}
|
278
|
+
end
|
279
|
+
# Returns an array of RevisionProperty objects (name, value) for revisions currently set on the given +rev+
|
280
|
+
# Tessted by: ../../test/subversion_test.rb:test_revision_properties
|
281
|
+
def self.revision_properties(rev)
|
282
|
+
revision_properties_names(rev).map { |property_name|
|
283
|
+
RevisionProperty.new(property_name, get_revision_property(property_name, rev))
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
def self.make_directory(dir)
|
288
|
+
execute "mkdir #{dir}"
|
289
|
+
end
|
290
|
+
|
291
|
+
def self.help(*args)
|
292
|
+
execute "help #{args.join(' ')}"
|
293
|
+
end
|
294
|
+
|
295
|
+
# Returns the raw output from svn log
|
296
|
+
def self.log(*args)
|
297
|
+
args = ['./'] if args.empty?
|
298
|
+
execute "log #{args.join(' ')}"
|
299
|
+
end
|
300
|
+
# Returns the revision number for head.
|
301
|
+
def self.latest_revision(*args)
|
302
|
+
args = ['./'] if args.empty?
|
303
|
+
# The revision returned by svn status -u seems to be a pretty reliable way to get this. Does anyone know of a better way?
|
304
|
+
matches = /Status against revision:\s+(\d+)/m.match(status_against_server(args))
|
305
|
+
matches && matches[1]
|
306
|
+
end
|
307
|
+
# Returns the revision number for the working directory(/file?) specified by +path+
|
308
|
+
def self.latest_revision_for_path(path)
|
309
|
+
# The revision returned by svn info seems to be a pretty reliable way to get this. Does anyone know of a better way?
|
310
|
+
matches = info(path).match(/^Revision: (\d+)/)
|
311
|
+
matches && matches[1]
|
312
|
+
end
|
313
|
+
|
314
|
+
# Returns an array of RSCM::Revision objects
|
315
|
+
def self.revisions(*args)
|
316
|
+
# Tried using this, but it seems to expect you to pass in a starting date or accept the default starting date of right now, which is silly if you actually just want *all* revisions...
|
317
|
+
#@rscm = ::RSCM::Subversion.new
|
318
|
+
#@rscm.revisions
|
319
|
+
|
320
|
+
#log_output = Subversion.log('-v')
|
321
|
+
log_output = Subversion.log(*(['-v'] + args))
|
322
|
+
parser = ::RSCM::SubversionLogParser.new(io = StringIO.new(log_output), url = 'http://ignore.me.com')
|
323
|
+
revisions = parser.parse_revisions
|
324
|
+
revisions
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
def self.info(*args)
|
329
|
+
args = ['./'] if args.empty?
|
330
|
+
execute "info #{args.join(' ')}"
|
331
|
+
end
|
332
|
+
|
333
|
+
def self.url(path_or_url = './')
|
334
|
+
matches = info(path_or_url).match(/^URL: (.+)/)
|
335
|
+
matches && matches[1]
|
336
|
+
end
|
337
|
+
|
338
|
+
# :todo: needs some serious unit-testing love
|
339
|
+
def self.base_url(path_or_url = './')
|
340
|
+
matches = info(path_or_url).match(/^Repository Root: (.+)/)
|
341
|
+
matches && matches[1]
|
342
|
+
|
343
|
+
# It appears that we might need to use this old way (which looks at 'URL'), since there is actually a handy property called "Repository Root" that we can look at.
|
344
|
+
# base_url = nil # needed so that base_url variable isn't local to loop block (and reset during next iteration)!
|
345
|
+
# started_using_dot_dots = false
|
346
|
+
# loop do
|
347
|
+
# matches = /^URL: (.+)/.match(info(path_or_url))
|
348
|
+
# if matches && matches[1]
|
349
|
+
# base_url = matches[1]
|
350
|
+
# else
|
351
|
+
# break base_url
|
352
|
+
# end
|
353
|
+
#
|
354
|
+
# # Keep going up the path, one directory at a time, until `svn info` no longer returns a URL (will probably eventually return 'svn: PROPFIND request failed')
|
355
|
+
# if path_or_url.include?('/') && !started_using_dot_dots
|
356
|
+
# path_or_url = File.dirname(path_or_url)
|
357
|
+
# else
|
358
|
+
# started_using_dot_dots = true
|
359
|
+
# path_or_url = File.join(path_or_url, '..')
|
360
|
+
# end
|
361
|
+
# #puts 'going up to ' + path_or_url
|
362
|
+
# end
|
363
|
+
end
|
364
|
+
def self.root_url(*args); base_url(*args); end
|
365
|
+
def self.repository_root(*args); base_url(*args); end
|
366
|
+
|
367
|
+
|
368
|
+
def self.repository_uuid(path_or_url = './')
|
369
|
+
matches = info(path_or_url).match(/^Repository UUID: (.+)/)
|
370
|
+
matches && matches[1]
|
371
|
+
end
|
372
|
+
|
373
|
+
# By default, if you query a directory that is scheduled for addition but hasn't been committed yet (node doesn't have a UUID),
|
374
|
+
# then we will still return true, because it is *scheduled* to be under version control. If you want a stricter definition,
|
375
|
+
# and only want it to return true if the file exists in the *repository* (has a UUID)@ then pass strict = true
|
376
|
+
def self.under_version_control?(file = './', strict = false)
|
377
|
+
if strict
|
378
|
+
!!repository_uuid(file)
|
379
|
+
else # (scheduled_for_addition_counts_as_true)
|
380
|
+
!!url(file)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
def self.working_copy_root(directory = './')
|
384
|
+
uuid = repository_uuid(directory)
|
385
|
+
return nil if uuid.nil?
|
386
|
+
|
387
|
+
loop do
|
388
|
+
# Keep going up, one level at a time, ...
|
389
|
+
new_directory = File.expand_path(File.join(directory, '..'))
|
390
|
+
new_uuid = repository_uuid(new_directory)
|
391
|
+
|
392
|
+
# Until we get back a uuid that is nil (it's not a working copy at all) or different (you can have a working copy A inside of a different WC B)...
|
393
|
+
break if new_uuid.nil? or new_uuid != uuid
|
394
|
+
|
395
|
+
directory = new_directory
|
396
|
+
end
|
397
|
+
directory
|
398
|
+
end
|
399
|
+
|
400
|
+
# The location of the executable to be used
|
401
|
+
# to do: We should find a smarter way to do this. (Could cache this result in .subwrap or somewhere, so we don't have to do all this work on every invocation...)
|
402
|
+
def self.executable
|
403
|
+
@@executable ||=
|
404
|
+
ENV['PATH'].split(windows_platform? ? ';' : ':').each do |dir|
|
405
|
+
executable = File.join(dir, (windows_platform? ? 'svn.exe' : 'svn'))
|
406
|
+
if File.exist?(executable) and !self.ruby_script?(executable) # We want to wrap the svn binary provided by Subversion, not our custom replacement for that.
|
407
|
+
return windows_platform? ? %{"#{executable}"} : executable
|
408
|
+
end
|
409
|
+
end
|
410
|
+
raise 'svn binary not found'
|
411
|
+
end
|
412
|
+
|
413
|
+
def self.ruby_script?(file_path)
|
414
|
+
if windows_platform?
|
415
|
+
# The 'file' command, we assume, is not available
|
416
|
+
File.readlines(file_path)[0] =~ /ruby/
|
417
|
+
else
|
418
|
+
`file #{file_path}` =~ /ruby/
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
protected
|
423
|
+
def self.execute(*args)
|
424
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
425
|
+
method = options.delete(:method) || :capture
|
426
|
+
|
427
|
+
command = "#{executable} #{args.join ' '}"
|
428
|
+
actually_execute(method, command)
|
429
|
+
end
|
430
|
+
# This abstraction exists to assist with unit tests. Test cases can simply override this function so that no external commands need to be executed.
|
431
|
+
def self.actually_execute(method, command)
|
432
|
+
if Subversion.dry_run && !$ignore_dry_run_option
|
433
|
+
puts "In execute(). Was about to execute this command via method :#{method}:"
|
434
|
+
p command
|
435
|
+
end
|
436
|
+
if Subversion.print_commands
|
437
|
+
p command
|
438
|
+
end
|
439
|
+
|
440
|
+
valid_options = [:capture, :exec, :popen]
|
441
|
+
case method
|
442
|
+
|
443
|
+
when :capture
|
444
|
+
`#{command} 2>&1`
|
445
|
+
|
446
|
+
when :exec
|
447
|
+
#Kernel.exec *args
|
448
|
+
Kernel.exec command
|
449
|
+
|
450
|
+
when :system
|
451
|
+
Kernel.system command
|
452
|
+
|
453
|
+
when :popen
|
454
|
+
# This is just an idea of how maybe we could improve the LATENCY. Rather than waiting until the command completes
|
455
|
+
# (which can take quite a while for svn status sometimes since it has to walk the entire directory tree), why not process
|
456
|
+
# the output from /usr/bin/svn *in real-time*??
|
457
|
+
#
|
458
|
+
# Unfortunately, it looks like /usr/bin/svn itself might make that impossible. It seems that if it detects that its output is
|
459
|
+
# being redirected to a pipe, it will not yield any output until the command is finished!
|
460
|
+
#
|
461
|
+
# So even though this command gives you output in real-time:
|
462
|
+
# find / | grep .
|
463
|
+
# as does this:
|
464
|
+
# IO.popen('find /', 'r') {|p| line = ""; ( puts line; $stdout.flush ) until !(line = p.gets) }
|
465
|
+
# as does this:
|
466
|
+
# /usr/bin/svn st
|
467
|
+
#
|
468
|
+
# ... as soon as you redirect svn to a *pipe*, it seems to automatically (annoyingly) buffer its output until it's finished:
|
469
|
+
# /usr/bin/svn st | grep .
|
470
|
+
# So when I tried this:
|
471
|
+
# IO.popen('/usr/bin/svn st', 'r') {|p| line = ""; ( puts line; $stdout.flush ) until !(line = p.gets) }
|
472
|
+
# it didn't seem any more responsive than a plain puts `/usr/bin/svn st` ! Frustrating!
|
473
|
+
#
|
474
|
+
IO.popen(command, 'r') do |pipe|
|
475
|
+
line = ""
|
476
|
+
( puts line; $stdout.flush ) until !(line = pipe.gets)
|
477
|
+
end
|
478
|
+
else
|
479
|
+
raise ArgumentError.new(":method option must be one of #{valid_options.inspect}")
|
480
|
+
end unless (Subversion.dry_run && !$ignore_dry_run_option)
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
|
485
|
+
|
486
|
+
|
487
|
+
|
488
|
+
|
489
|
+
|
490
|
+
|
491
|
+
|
492
|
+
#-----------------------------------------------------------------------------------------------------------------------------
|
493
|
+
module Subversion
|
494
|
+
RevisionProperty = Struct.new(:name, :value)
|
495
|
+
|
496
|
+
# Represents an "externals container", which is a directory that has the <tt>svn:externals</tt> property set to something useful.
|
497
|
+
# Each ExternalsContainer contains a set of "entries", which are the actual directories listed in the <tt>svn:externals</tt>
|
498
|
+
# property and are "pulled into" the directory.
|
499
|
+
class ExternalsContainer
|
500
|
+
ExternalItem = Struct.new(:name, :repository_path)
|
501
|
+
attr_reader :container_dir
|
502
|
+
attr_reader :entries
|
503
|
+
|
504
|
+
def initialize(external_dir)
|
505
|
+
@container_dir = File.expand_path(external_dir)
|
506
|
+
@entries = Subversion.get_property("externals", @container_dir)
|
507
|
+
#p @entries
|
508
|
+
end
|
509
|
+
|
510
|
+
def has_entries?
|
511
|
+
@entries.size > 0
|
512
|
+
end
|
513
|
+
|
514
|
+
def entries_structs
|
515
|
+
entries.chomp.enum(:each_line).map { |line|
|
516
|
+
line =~ /^(\S+)\s*(\S+)/
|
517
|
+
ExternalItem.new($1, $2)
|
518
|
+
}
|
519
|
+
end
|
520
|
+
|
521
|
+
def to_s
|
522
|
+
entries_structs = entries_structs()
|
523
|
+
longest_item_name =
|
524
|
+
[
|
525
|
+
entries_structs.map { |entry|
|
526
|
+
entry.name.size
|
527
|
+
}.max.to_i,
|
528
|
+
25
|
529
|
+
].max
|
530
|
+
|
531
|
+
container_dir.bold + "\n" +
|
532
|
+
entries_structs.map { |entry|
|
533
|
+
" * " + entry.name.ljust(longest_item_name + 1) + entry.repository_path + "\n"
|
534
|
+
}.join
|
535
|
+
end
|
536
|
+
|
537
|
+
def ==(other)
|
538
|
+
self.container_dir == other.container_dir
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# A collection of Diff objects in in file_name => diff format.
|
543
|
+
class Diffs < Hash
|
544
|
+
end
|
545
|
+
|
546
|
+
class Diff
|
547
|
+
attr_reader :filename, :diff
|
548
|
+
initializer :filename do
|
549
|
+
@diff = ''
|
550
|
+
end
|
551
|
+
def filename_pretty
|
552
|
+
filename.ljust(100).black_on_white
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
class DiffsParser
|
557
|
+
class ParseError < Exception; end
|
558
|
+
initializer :raw_diffs
|
559
|
+
@state = nil
|
560
|
+
def parse
|
561
|
+
diffs = Diffs.new
|
562
|
+
current_diff = nil
|
563
|
+
@raw_diffs.each_line do |line|
|
564
|
+
if line =~ /^Index: (.*)$/
|
565
|
+
current_diff = Diff.new($1)
|
566
|
+
diffs[current_diff.filename] = current_diff #unless current_diff.nil?
|
567
|
+
@state = :immediately_after_filename
|
568
|
+
next
|
569
|
+
end
|
570
|
+
|
571
|
+
if current_diff.nil?
|
572
|
+
raise ParseError.new("The raw diff input didn't begin with 'Index:'!")
|
573
|
+
end
|
574
|
+
|
575
|
+
if @state == :immediately_after_filename
|
576
|
+
if line =~ /^===================================================================$/ ||
|
577
|
+
line =~ /^---.*\(revision \d+\)$/ ||
|
578
|
+
line =~ /^\+\+\+.*\(revision \d+\)$/ ||
|
579
|
+
line =~ /^@@ .* @@$/
|
580
|
+
# Skip
|
581
|
+
next
|
582
|
+
else
|
583
|
+
@state= :inside_the_actual_diff
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
if @state == :inside_the_actual_diff
|
588
|
+
current_diff.diff << line
|
589
|
+
else
|
590
|
+
raise ParseError.new("Expected to be in :inside_the_actual_diff state, but was not.")
|
591
|
+
end
|
592
|
+
end
|
593
|
+
diffs.freeze
|
594
|
+
diffs
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
end
|
599
|
+
end
|