subwrap 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ gem 'facets', '>=1.8.20'
5
+ require 'facets/core/kernel/require_local'
6
+ require_local '../lib/subwrap/svn_command.rb'
7
+ Subversion::SvnCommand.execute
data/bin/svn ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ gem 'facets', '>=1.8.20'
5
+ require 'facets/core/kernel/require_local'
6
+ require_local '../lib/subwrap/svn_command.rb'
7
+ Subversion::SvnCommand.execute
@@ -0,0 +1,6 @@
1
+ # This is the auto-require
2
+ require 'rubygems'
3
+ gem 'facets'
4
+ require 'facets/core/kernel/require_local'
5
+ require_local 'subwrap/subversion'
6
+ Subversion
@@ -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