subwrap 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,148 @@
1
+ # Tested by: ../../test/subversion_extensions_test.rb
2
+
3
+ gem 'colored'
4
+ require 'colored'
5
+
6
+ require 'facets/core/module/class_extension'
7
+
8
+ class Array
9
+ def to_regexp_char_class
10
+ "[#{join('')}]"
11
+ end
12
+ end
13
+
14
+ class String
15
+ def colorize_svn_question_mark; self.yellow.bold; end
16
+ def colorize_svn_add; self.green.bold; end
17
+ def colorize_svn_modified; self.cyan.bold; end
18
+ def colorize_svn_updated; self.yellow.bold; end
19
+ def colorize_svn_deleted; self.magenta.bold; end
20
+ def colorize_svn_conflict; self.red.bold; end
21
+ def colorize_svn_tilde; self.red.bold; end
22
+ def colorize_svn_exclamation; self.red.bold; end
23
+
24
+ def colorize_svn_status_code
25
+ if Subversion.color
26
+ self.gsub('?') { $&.colorize_svn_question_mark }.
27
+ gsub('A') { $&.colorize_svn_add }.
28
+ gsub('M') { $&.colorize_svn_modified }.
29
+ gsub('D') { $&.colorize_svn_deleted }.
30
+ gsub('C') { $&.colorize_svn_conflict }.
31
+ gsub('~') { $&.colorize_svn_tilde }.
32
+ gsub('!') { $&.colorize_svn_exclamation }
33
+ else
34
+ self
35
+ end
36
+ end
37
+ def colorize_svn_status_lines
38
+ if Subversion.color
39
+ self.gsub(/^ *([^ ])\s/) { $&.colorize_svn_status_code }
40
+ else
41
+ self
42
+ end
43
+ end
44
+ def colorize_svn_update_lines
45
+ if Subversion.color
46
+ self.gsub(/^ *U\s/) { $&.colorize_svn_updated }.
47
+ gsub(/^ *A\s/) { $&.colorize_svn_add }.
48
+ gsub(/^ *M\s/) { $&.colorize_svn_modified }.
49
+ gsub(/^ *D\s/) { $&.colorize_svn_deleted }.
50
+ gsub(/^ *C\s/) { $&.colorize_svn_conflict }
51
+ else
52
+ self
53
+ end
54
+ end
55
+ def colorize_svn_diff
56
+ if Subversion.color
57
+ self.gsub(/^(Index: )(.*)$/) { $2.ljust(100).black_on_white}.
58
+ gsub(/^=+\n/, '')
59
+ else
60
+ self
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ # These are methods used by the SvnCommand for filtering and whatever else it needs...
67
+ # It could probably be moved into SvnCommand, but I thought it might be good to at least make it *possible* to use them apart from SvnCommand.
68
+ # Rename to Subversion::Filters ? Then each_unadded would be an odd man out.
69
+ module Subversion
70
+ module Extensions
71
+ Interesting_status_flags = ["M", "A", "D", "?"]
72
+ Uninteresting_status_flags = ["X", "W"]
73
+ Status_flags = Interesting_status_flags | Uninteresting_status_flags
74
+
75
+ class_extension do # These are actually class methods, but we have to do it this way so that the Subversion.extend(Subversion::Extensions) will also add these class methods to Subversion.
76
+
77
+ def status_lines_filter(input)
78
+ input = (input || "").reject { |line|
79
+ line =~ /^$/ # Blank lines
80
+ }.reject { |line|
81
+ line =~ /^#{Uninteresting_status_flags.to_regexp_char_class}/
82
+ }.join
83
+
84
+ before_externals, *externals = input.split(/^Performing status on external item at.*$/)
85
+
86
+ before_externals ||= ''
87
+ before_externals = before_externals.strip.colorize_svn_status_lines + "\n" if before_externals != ""
88
+
89
+ externals = externals.join.strip
90
+ externals =
91
+ '_'*40 + ' externals '.underline + '_'*40 + "\n" +
92
+ externals.reject { |line|
93
+ line =~ /^Performing status on external item at/
94
+ }.reject { |line|
95
+ line =~ /^$/ # Blank lines
96
+ }.join.strip.colorize_svn_status_lines + "\n" if externals != ""
97
+
98
+ before_externals +
99
+ externals
100
+ end
101
+
102
+ def update_lines_filter(input)
103
+ input.reject { |line|
104
+ line =~ /^$/ # Blank lines
105
+ }.reject { |line|
106
+ line =~ /^Fetching external item into/
107
+ # Eventually we may want it to include this whole block, but only iff there is something updated for this external.
108
+ }.reject { |line|
109
+ line =~ /^External at revision/
110
+ }.join.colorize_svn_update_lines
111
+ # Also get rid of all but one "At revision _."?
112
+ end
113
+
114
+ def unadded_lines_filter(input)
115
+ input.select { |line|
116
+ line =~ /^\?/
117
+ }.join
118
+ end
119
+ def unadded_filter(input)
120
+ unadded_lines_filter(input).map { |line|
121
+ # Just keep the filename part
122
+ line =~ /^\?\s+(.+)/
123
+ $1
124
+ }
125
+ end
126
+
127
+ def each_unadded(input)
128
+ unadded_filter(input).each { |line|
129
+ yield line
130
+ }
131
+ end
132
+
133
+ # This is just a wrapper for Subversion::diff that adds some color
134
+ def colorized_diff(*args)
135
+ Subversion::diff(*args).colorize_svn_diff.add_exit_code_error
136
+ end
137
+
138
+ # A wrapper for Subversion::revision_properties that formats it for display on srceen
139
+ def printable_revision_properties(rev)
140
+ Subversion::revision_properties(rev).map do |property|
141
+ "#{property.name.ljust(20)} = '#{property.value}'"
142
+ end.join("\n")
143
+ end
144
+
145
+ end # class_extension
146
+
147
+ end # module Extensions
148
+ end # module Subversion
@@ -0,0 +1,1568 @@
1
+ # Tested by: ../../test/svn_command_test.rb
2
+
3
+ require 'rubygems'
4
+
5
+ gem 'facets', '>=1.8.51'
6
+ #require 'facets/more/command' # Not until they include my changes
7
+ require 'facets/core/kernel/require_local'
8
+ require 'facets/core/kernel/with' # returning
9
+ require 'facets/core/enumerable/every'
10
+ require 'facets/core/array/select' # select!
11
+ require 'facets/core/string/margin'
12
+ require 'facets/core/string/lines'
13
+ require 'facets/core/string/index_all'
14
+ require 'facets/core/string/to_re'
15
+ require 'facets/core/string/to_rx'
16
+ require 'facets/core/symbol/to_proc'
17
+ require 'facets/core/kernel/in'
18
+
19
+ gem 'quality_extensions', '>=0.0.3'
20
+ require 'quality_extensions/enumerable/enum'
21
+ require 'quality_extensions/array/expand_ranges'
22
+ require 'quality_extensions/array/shell_escape'
23
+ require 'quality_extensions/file_test/binary_file'
24
+ require 'quality_extensions/console/command'
25
+ require 'quality_extensions/module/attribute_accessors'
26
+ #require 'quality_extensions/module/class_methods'
27
+
28
+ require 'English'
29
+ require 'pp'
30
+ require 'stringio'
31
+
32
+ gem 'colored'
33
+ require 'colored' # Lets us do "a".white.bold instead of "\033[1ma\033[0m"
34
+
35
+ require_local '../../ProjectInfo'
36
+ require_local 'subversion'
37
+ require_local 'subversion_extensions'
38
+
39
+
40
+ begin
41
+ gem 'termios'
42
+ require 'termios'
43
+ begin
44
+ # Set up termios so that it returns immediately when you press a key.
45
+ # (http://blog.rezra.com/articles/2005/12/05/single-character-input)
46
+ t = Termios.tcgetattr(STDIN)
47
+ save_terminal_attributes = t.dup
48
+ t.lflag &= ~Termios::ICANON
49
+ Termios.tcsetattr(STDIN, 0, t)
50
+
51
+ # Set terminal_attributes back to how we found them...
52
+ at_exit { Termios.tcsetattr(STDIN, 0, save_terminal_attributes) }
53
+ rescue RuntimeError => exception # Necessary for automated testing.
54
+ if exception.message =~ /can't get terminal parameters/
55
+ puts 'Warning: Terminal not found.'
56
+ $interactive = false
57
+ else
58
+ raise
59
+ end
60
+ end
61
+ $termios_loaded = true
62
+ rescue Gem::LoadError
63
+ $termios_loaded = false
64
+ end
65
+
66
+
67
+ module Kernel
68
+ # Simply to allow us to override it
69
+ # Should be renamed to exit_status maybe since it returns a Process::Status
70
+ def exit_code
71
+ $?
72
+ end
73
+ end
74
+
75
+ class Object
76
+ def nonnil?; !nil?; end
77
+ end
78
+
79
+ class IO
80
+ # Gets a single character, as a string.
81
+ # Adjusts for the different behavior of getc if we are using termios to get it to return immediately when you press a single key
82
+ # or if they are not using that behavior and thus have to press Enter after their single key.
83
+ def getch
84
+ response = getc
85
+ if !$termios_loaded
86
+ next_char = getc
87
+ new_line_characters_expected = ["\n"]
88
+ #new_line_characters_expected = ["\n", "\r"] if windows?
89
+ if next_char.chr.in?(new_line_characters_expected)
90
+ # Eat the newline character
91
+ else
92
+ # Don't eat it
93
+ # (This case is necessary, for escape sequences, for example, where they press only one key, but it produces multiple characters.)
94
+ $stdin.ungetc(next_char)
95
+ end
96
+ end
97
+ response.chr
98
+ end
99
+ end
100
+
101
+
102
+ class String
103
+ # Makes the first character bold and underlined. Makes the whole string of the given color.
104
+ # :todo: Move out to extensions/console/menu_item
105
+ def menu_item(color = :white, letter = self[0..0], which_occurence = 0)
106
+ index = index_all(/#{letter}/)[which_occurence]
107
+ raise "Could not find a #{which_occurence}th occurence of '#{letter}' in string '#{self}'" if index.nil?
108
+ before = self[0..index-1].send(color) unless index == 0
109
+ middle = self[index..index].send(color).bold.underline
110
+ after = self[index+1..-1].send(color)
111
+ before.to_s + middle + after
112
+ end
113
+ # Extracted so that we can override it for tests. Otherwise it will have a NoMethodError because $? will be nil because it will not have actually executed any commands.
114
+ def add_exit_code_error
115
+ self << "Exited with error!".bold.red if !exit_code.success?
116
+ self
117
+ end
118
+ def relativize_path
119
+ self.gsub(File.expand_path(FileUtils.getwd) + '/', '') # Simplify the directory by removing the working directory from it, if possible
120
+ end
121
+ def highlight_occurences(search_pattern, color = :red)
122
+ self.gsub(search_pattern) { $&.send(color).bold }
123
+ end
124
+ end
125
+ def confirm(question, options = ['Yes', 'No'])
126
+ print question + " " +
127
+ "Yes".menu_item(:red) + ", " +
128
+ "No".menu_item(:green) +
129
+ " > "
130
+ response = ''
131
+ # Currently allow user to press Enter to accept the default.
132
+ response = $stdin.getch.downcase while !['y', 'n', "\n"].include?(begin response.downcase!; response end)
133
+ response
134
+ end
135
+
136
+ #Subversion.extend(Subversion::Extensions)
137
+ module Subversion
138
+ include(Subversion::Extensions)
139
+ end
140
+ Subversion::color = true
141
+
142
+ module Subversion
143
+ class SvnCommand < Console::Command
144
+ end
145
+ end
146
+
147
+ # Handle user preferences
148
+ module Subversion
149
+ class SvnCommand
150
+ @@user_preferences = {}
151
+ mattr_accessor :user_preferences
152
+ end
153
+ end
154
+ if File.exists?(user_preference_file = "#{ENV['HOME']}/.subwrap.yml")
155
+ Subversion::SvnCommand.user_preferences = YAML::load(IO.read(user_preference_file)) || {}
156
+ end
157
+
158
+ module Subversion
159
+ class SvnCommand
160
+
161
+ # Constants
162
+ C_standard_remote_command_options = {
163
+ [:__username] => 1,
164
+ [:__password] => 1,
165
+ [:__no_auth_cache] => 0,
166
+ [:__non_interactive] => 0,
167
+ [:__config_dir] => 1,
168
+ }
169
+ C_standard_commitable_command_options = {
170
+ [:_m, :__message] => 1,
171
+ [:_F, :__file] => 1,
172
+ [:__force_log] => 0,
173
+ [:__editor_cmd] => 1,
174
+ [:__encoding] => 1,
175
+ }
176
+
177
+ # This shouldn't be necessary. Console::Command should allow introspection. But until such time...
178
+ @@subcommand_list = [
179
+ 'each_unadded',
180
+ 'externals_items', 'externals_outline', 'externals_containers', 'edit_externals', 'externalize',
181
+ 'ignore', 'edit_ignores',
182
+ 'revisions',
183
+ 'get_message', 'set_message', 'edit_message',
184
+ 'view_commits',
185
+ 'url',
186
+ 'repository_root', 'working_copy_root', 'repository_uuid',
187
+ 'latest_revision',
188
+ 'delete_svn',
189
+ 'fix_out_of_date_commit_state'
190
+ ]
191
+ mattr_reader :subcommand_list
192
+
193
+ def initialize(*args)
194
+ @passthrough_options = []
195
+ super
196
+ end
197
+
198
+ #-----------------------------------------------------------------------------------------------------------------------------
199
+ # Global options
200
+
201
+ global_option :__no_color, :__dry_run, :__debug, :__print_commands
202
+ def __no_color
203
+ Subversion::color = false
204
+ end
205
+ def __debug
206
+ $debug = true
207
+ end
208
+ def __dry_run
209
+ Subversion::dry_run = true
210
+ end
211
+
212
+ def __print_commands
213
+ Subversion::print_commands = true
214
+ end
215
+ alias_method :__show_commands, :__print_commands
216
+ # Don't want to hide/conflict with svn's own -v/--verbose flag, so using capital initial letter
217
+ alias_method :_V, :__print_commands
218
+ alias_method :__Verbose, :__print_commands
219
+
220
+ # Usually most Subversion commands are recursive and all-inclusive. This option adds file *exclusion* to most of Subversion's commands.
221
+ # Use this if you want to commit (/add/etc.) everything *but* a certain file or set of files
222
+ # svn commit dir1 dir2 --except dir1/not_ready_yet.rb
223
+ def __except
224
+ # We'll have to use a FileList to do this. This option will remove all file arguments, put them into a FileList as inclusions,
225
+ # add the exclusions, and then pass the resulting list of files on to the *actual* svn command.
226
+ # :todo:
227
+ end
228
+ alias_method :__exclude, :__except
229
+
230
+ #-----------------------------------------------------------------------------------------------------------------------------
231
+ # Default/dynamic behavior
232
+
233
+ # Any subcommands that we haven't implemented here will simply be passed on to the built-in svn command.
234
+ # :todo: Distinguish between subcommand_missing and method_missing !
235
+ # Currently, for example, if as isn't defined, this: puts Subversion.externalize(repo_path, {:as => as })
236
+ # will call method_missing and try to run `svn as`, which of course will fail (without a sensible relevant error)...
237
+ # I think we should probably just have a separate subcommand_missing, like we already have a separate option_missing !!!
238
+ # Even a simple type (sss instead of ss) causes trouble... *this* was causing a call to "/usr/bin/svn new_messsage" -- what huh??
239
+ # def set_message(new_message = nil)
240
+ # args << new_messsage if new_message
241
+ def method_missing(subcommand, *args)
242
+ #puts "method_missing(#{subcommand}, #{args.inspect})"
243
+ svn :exec, subcommand, *args
244
+ end
245
+ # This is here solely to allow subcommandless commands like `svn --version`
246
+ def default()
247
+ svn :exec
248
+ end
249
+
250
+ def option_missing(option_name, args)
251
+ #puts "#{@subcommand} defined? #{@subcommand_is_defined}"
252
+ if !@subcommand_is_defined
253
+ # It's okay to use this for pass-through subcommands, because we just pass all options/arguments verbatim anyway...
254
+ #puts "option_missing(#{option_name}, #{args.inspect})"
255
+ else
256
+ # But for subcommands that are defined here, we should know better! All valid options should be explicitly listed!
257
+ raise UnknownOptionError.new(option_name)
258
+ end
259
+
260
+ # The following is necessary because we really don't know the arity (how many subsequent tokens it should eat) of the option -- we don't know anything about the options, in fact; that's why we've landed in option_missing.
261
+ # This is kind of a hokey solution, but for any unrecognized options/args (which will be *all* of them unless we list the available options in the subcommand module), we just eat all of the args, store them in @passthrough_options, and later we will add them back on.
262
+ # What's annoying about it this solution is that *everything* after the first unrecognized option comes in as args, even if they are args for the subcommand and not for the *option*!
263
+ # But...it seems to work to just pretend they're options.
264
+ # It seems like this is mostly a problem for *wrappers* that try to use Console::Command. Sometimes you just want to *pass through all args and options* unchanged and just filter the output somehow.
265
+ # Command doesn't make that super-easy though. If an option (--whatever) isn't defined, then the only way to catch it is in option_missing. And since we can't the arity unless we enumerate all options, we have to hokily treat the first option as having unlimited arity.
266
+ # Alternatives considered:
267
+ # * Assume arity of 0. Then I'm afraid it would extract out all the option flags and leave the args that were meant for the args dangling there out of order ("-r 1 -m 'hi'" => "-r -m", "1 'hi'")
268
+ # * Assume arity of 1. Then if it was really 0, it would pick up an extra arg that really wasn't supposed to be an arg for the *option*.
269
+ # Ideally, we wouldn't be using option_missing at all because all options would be listed in the respective subcommand module...but for subcommands handled through method_missing, we don't have that option.
270
+
271
+ # The args will look like this, for example:
272
+ # option_missing(-m, ["a multi-word message", "--something-else", "something else"])
273
+ # , so we need to be sure we wrap multi-word args in quotes as necessary. That's what the args.shell_escape does.
274
+
275
+ @passthrough_options << "#{option_name}" << args.shell_escape
276
+ @passthrough_options.flatten! # necessary now that we have args.shell_escape ?
277
+
278
+ return arity = args.size # All of 'em
279
+ end
280
+
281
+ #-----------------------------------------------------------------------------------------------------------------------------
282
+ # Built-in commands (in alphabetical order)
283
+
284
+ #-----------------------------------------------------------------------------------------------------------------------------
285
+ module Add
286
+ Console::Command.pass_through({
287
+ [:__targets] => 1,
288
+ [:_N, :__non_recursive] => 0,
289
+ [:_q, :__quiet] => 0,
290
+ [:__config_dir] => 1,
291
+ [:__force] => 0,
292
+ [:__no_ignore] => 0,
293
+ [:__auto_props] => 0,
294
+ [:__no_auto_props] => 0,
295
+ }, self)
296
+ end
297
+ def add(*args)
298
+ #puts "add #{args.inspect}"
299
+ svn :exec, 'add', *args
300
+ end
301
+
302
+ #-----------------------------------------------------------------------------------------------------------------------------
303
+ module Commit
304
+ Console::Command.pass_through({
305
+ [:_q, :__quiet] => 0,
306
+ [:_N, :__non_recursive] => 1,
307
+ [:__targets] => 1,
308
+ [:__no_unlock] => 0,
309
+ }.
310
+ merge(SvnCommand::C_standard_remote_command_options).
311
+ merge(SvnCommand::C_standard_commitable_command_options), self
312
+ )
313
+
314
+ # Use this flag if you don't want a commit notification to be sent out.
315
+ def __skip_notification
316
+ @skip_notification = true
317
+ end
318
+ alias_method :__covert, :__skip_notification
319
+ alias_method :__minor_edit, :__skip_notification # Like in MediaWiki. If you're just fixing a typo or something, then most people probably don't want to hear about it.
320
+ alias_method :__minor, :__skip_notification
321
+
322
+ # Use this flag if you are about to commit some code for which you know the tests aren't or (probaby won't) pass.
323
+ # This *may* cause your continuous integration system to either skip tests for this revision or at least be a little more
324
+ # *leniant* towards you (a slap on the wrist instead of a public flogging, perhaps) when it runs the tests and finds that
325
+ # they *are* failing.
326
+ # You should probably only do this if you are planning on making multiple commits in rapid succession (sometimes Subversion
327
+ # forces you to do an intermediate commit in order to move something that's already been scheduled for a move or somethhing,
328
+ # for example). If things will be broken for a while, consider starting a branch for your changes and merging the branch
329
+ # back into trunk only when you've gotten the code stable/working again.
330
+ # (See http://svn.collab.net/repos/svn/trunk/doc/user/svn-best-practices.html)
331
+ def __broken
332
+ @broken = true
333
+ end
334
+ alias_method :__expect_to_break_tests, :__broken
335
+ alias_method :__knowingly_committing_broken_code, :__broken
336
+
337
+ # Skips e-mail and marks reviewed=true
338
+ # Similar to the 'reviewed' command, which just marks reviewed=true
339
+ def __doesnt_need_review
340
+ @__doesnt_need_review = true
341
+ end
342
+
343
+ # :todo: svn doesn't allow you to commit changes to externals in the same transaction as your "main working copy", but we
344
+ # can provide the illusion that this is possible, by doing multiple commits, one for each working copy/external.
345
+ #
346
+ # When this option is used, the same commit message is used for all commits.
347
+ #
348
+ # Of course, this may not be what the user wants; the user may wish to specify a different commit message for the externals
349
+ # than for the "main working copy", in which case the user should not be using this option!
350
+ def __include_externals
351
+ @include_externals = true
352
+ end
353
+
354
+ # Causes blame/author for this commit/file to stay the same as previous revision of the file
355
+ # Useful to workaround bug where fixing indent and other inconsequential changes causes you to be displayed as the author if you do a blame, [hiding] the real author
356
+ def __shirk_blame
357
+ @shirk_blame = true
358
+ end
359
+
360
+ end #module Commit
361
+
362
+ def commit(*args)
363
+ directory = args.first || './' # We can only pass one path to .latest_revision and .repository_root, so we'll just arbitrarily choose the first path. They should all be paths within the same repository anyway, so it shouldn't matter.
364
+ if @broken || @skip_notification
365
+ latest_rev_before_commit = Subversion.latest_revision(directory)
366
+ repository_root = Subversion.repository_root(directory)
367
+ end
368
+
369
+ Subversion.print_commands! do
370
+ puts svn(:capture, "propset svn:skip_commit_notification_for_next_commit true --revprop -r #{latest_rev_before_commit} #{repository_root}", :prepare_args => false)
371
+ end if @skip_notification
372
+ # :todo:
373
+ # Add some logic to automatically skip the commit e-mail if the size of the files to be committed exceeds a threshold of __ MB.
374
+ # (Performance idea: Only check the size of the files if svn st includes (bin)?)
375
+
376
+ # Have to use :system rather than :capture because they may not have specified a commit message, in which case it will open up an editor...
377
+ svn(:system, 'commit', *(['--force-log'] + args))
378
+
379
+ puts ''.add_exit_code_error
380
+ return if !exit_code.success?
381
+
382
+ # The following only works if we do :capture (`svn`), but that doesn't work so well (at all) if svn tries to open up an editor (vim),
383
+ # which is what happens if you don't specify a message.:
384
+ # puts output = svn(:capture, 'commit', *(['--force-log'] + args))
385
+ # just_committed = (matches = output.match(/Committed revision (\d+)\./)) && matches[1]
386
+
387
+ Subversion.print_commands! do
388
+ puts svn(:capture, "propset code:broken true --revprop -r #{latest_rev_before_commit + 1}", :prepare_args => false)
389
+ end if @broken
390
+
391
+ if @include_externals
392
+ #:todo:
393
+ #externals.each do |external|
394
+ #svn(:system, 'commit', *(['--force-log'] + args + external))
395
+ #end
396
+ end
397
+
398
+
399
+ # This should be disableable! ~/.subwrap ?
400
+ # http://svn.collab.net/repos/svn/trunk/doc/user/svn-best-practices.html:
401
+ # After every svn commit, your working copy has mixed revisions. The things you just committed are now at the HEAD revision, and everything else is at an older revision.
402
+ #puts "Whenever you commit something, strangely, your working copy becomes out of date (as you can observe if you run svn info and look at the revision number). This is a problem for svn log, and piston, to name two applications. So we will now update '#{(args.every + '/..').join(' ').white.bold}' just to make sure they're not out of date..."
403
+ #print ''.bold # Clear the bold flag that svn annoyingly sets
404
+ #working_copy_root = Subversion.working_copy_root(directory).to_s
405
+ #response = confirm("Do you want to update #{working_copy_root.bold} now? (Any key other than y to skip) ")
406
+ #if response == 'y'
407
+ #puts "Updating #{working_copy_root} (non-recursively)..."
408
+ #end
409
+ #puts Subversion.update_lines_filter( Subversion.update(*args) )
410
+ end
411
+
412
+ # Ideas:
413
+ # * look for .svn-commit files within current tree and if one is found, show what's in it and ask
414
+ # "Found a commit message from a previous failed commit. {preview} Do you want to (u)se this message for the current commit, or (d)elete it?"
415
+
416
+ # A fix for this annoying problem that I seem to come across all too frequentrly:
417
+ # svn: Commit failed (details follow):
418
+ # svn: Your file or directory 'whatever.rb' is probably out-of-date
419
+ def fix_out_of_date_commit_state(dir)
420
+ dir = $1 if dir =~ %r|^(.*)/$| # Strip trailing slash.
421
+
422
+ puts Subversion.export("#{dir}", "#{dir}.new"). # Exports (copies) the contents of working copy 'dir' (including your uncommitted changes, don't worry! ... and you'll get a chance to confirm before anything is deleted; but sometimes although it exports files that are scheduled for addition, they are no longer scheduled for addition in the new working copy, so you have to re-add them) to non-working-copy 'dir.new'
423
+ add_exit_code_error
424
+ return if !exit_code.success?
425
+
426
+ system("mv #{dir} #{dir}.backup") # Just in case something goes ary
427
+ puts ''.add_exit_code_error
428
+ return if !exit_code.success?
429
+
430
+ puts "Restoring #{dir}..."
431
+ Subversion.update dir # Restore the directory to a pristine state so we will no longer get that annoying error
432
+
433
+ # Assure the user that dir.new really does have your latest changes
434
+ #puts "Here's a diff. Your changes/additions will be in the *right* (>) file."
435
+ #system("diff #{dir}.backup #{dir}")
436
+
437
+ # Merge those latest changes back into the pristine working copy
438
+ system("cp -R #{dir}.new/. #{dir}/")
439
+
440
+ # Assure the user one more time
441
+ puts Subversion.colorized_diff(dir)
442
+
443
+ puts "Please check the output of " + "svn st #{dir}.backup".blue.bold + " to check if any files were scheduled for addition. You will need to manually re-add these, as the export will have caused those files to lost their scheduling."
444
+ Subversion.print_commands! do
445
+ print Subversion.status_lines_filter( Subversion.status("#{dir}.backup") )
446
+ print Subversion.status_lines_filter( Subversion.status("#{dir}") )
447
+ end
448
+
449
+ # Actually commit
450
+ puts
451
+ response = confirm("Are you ready to try the commit again now?")
452
+ puts
453
+ if response == 'y'
454
+ puts "Great! Go for it. (I'd do it for you but I don't know what commit command you were trying to execute when the problem occurred.)"
455
+ end
456
+
457
+ # Clean up
458
+ #puts
459
+ #response = confirm("Do you want to delete array.backup array.new now?")
460
+ puts "Don't forget to " + "rm -rf #{dir}.backup #{dir}.new".blue.bold + " when you are done!"
461
+ #rm_rf array.backup, array.new
462
+ puts
463
+ end
464
+
465
+ #-----------------------------------------------------------------------------------------------------------------------------
466
+ module Diff
467
+ Console::Command.pass_through({
468
+ [:_r, :__revision] => 1, # :todo: support "{" DATE "}" format
469
+ [:__old] => 1,
470
+ [:__new] => 0,
471
+ #[:_N, :__non_recursive] => 0,
472
+ [:__diff_cmd] => 1,
473
+ [:_x, :__extensions] => 1, # :todo: should support any number of args??
474
+ [:__no_diff_deleted] => 0,
475
+ [:__notice_ancestry] => 1,
476
+ [:__force] => 1,
477
+ }.merge(SvnCommand::C_standard_remote_command_options), self
478
+ )
479
+
480
+ def __non_recursive
481
+ @non_recursive = true
482
+ @passthrough_options << '--non-recursive'
483
+ end
484
+ alias_method :_N, :__non_recursive
485
+
486
+ def __ignore_externals
487
+ @ignore_externals = true
488
+ end
489
+ alias_method :_ie, :__ignore_externals
490
+ alias_method :_skip_externals, :__ignore_externals
491
+ end
492
+
493
+ def diff(*directories)
494
+ directories = ['./'] if directories.empty?
495
+ puts Subversion.colorized_diff(*(prepare_args(directories)))
496
+
497
+ begin # Show diff for externals (if there *are* any and the user didn't tell us to ignore them)
498
+ output = StringIO.new
499
+ #paths = args.reject{|arg| arg =~ /^-/} || ['./']
500
+ directories.each do |path|
501
+ (Subversion.externals_items(path) || []).each do |item|
502
+ diff_output = Subversion.colorized_diff(item).strip
503
+ unless diff_output == ""
504
+ #output.puts '-'*100
505
+ #output.puts item.ljust(100, ' ').black_on_white.bold.underline
506
+ output.puts item.ljust(100).yellow_on_red.bold
507
+ output.puts diff_output
508
+ end
509
+ end
510
+ end
511
+ unless output.string == ""
512
+ #puts '='*100
513
+ puts (' '*100).yellow.underline
514
+ puts " Diff of externals (**don't forget to commit these too!**):".ljust(100, ' ').yellow_on_red.bold.underline
515
+ puts output.string
516
+ end
517
+ end unless @ignore_externals || @non_recursive
518
+ end
519
+
520
+ #-----------------------------------------------------------------------------------------------------------------------------
521
+ module Help
522
+ Console::Command.pass_through({
523
+ [:__version] => 0,
524
+ [:_q, :__quiet] => 0,
525
+ [:__config_dir] => 1,
526
+ }, self)
527
+ end
528
+ def help(subcommand = nil)
529
+ case subcommand
530
+ when "externals"
531
+ puts %Q{
532
+ | externals (ext): Lists all externals in the given working directory.
533
+ | usage: externals [PATH]
534
+ }.margin
535
+ # :todo: Finish...
536
+
537
+ when nil
538
+ puts "You are using " +
539
+ 's'.green.bold + 'v'.cyan.bold + 'n'.magenta.bold + '-' + 'c'.red.bold + 'o'.cyan.bold + 'm'.blue.bold + 'm'.yellow.bold + 'a'.green.bold + 'n'.white.bold + 'd'.green.bold + ' version ' + Project::Version
540
+ ", a colorful, useful replacement/wrapper for the standard svn command."
541
+ puts "subwrap is installed at: " + $0.bold
542
+ puts "You may bypass this wrapper by using the full path to svn: " + Subversion.executable.bold
543
+ puts
544
+ puts Subversion.help(subcommand).gsub(<<End, '')
545
+
546
+ Subversion is a tool for version control.
547
+ For additional information, see http://subversion.tigris.org/
548
+ End
549
+
550
+ puts
551
+ puts 'Subcommands added by subwrap (refer to '.green.underline + 'http://subwrap.rubyforge.org/'.white.underline + ' for usage details):'.green.underline
552
+ @@subcommand_list.each do |subcommand|
553
+ aliases_list = subcommand_aliases_list(subcommand.option_methodize.to_sym)
554
+ aliases_list = aliases_list.empty? ? '' : ' (' + aliases_list.join(', ') + ')'
555
+ puts ' ' + subcommand + aliases_list
556
+ end
557
+ #p subcommand_aliases_list(:edit_externals)
558
+
559
+ else
560
+ #puts "help #{subcommand}"
561
+ puts Subversion.help(subcommand)
562
+ end
563
+ end
564
+
565
+ #-----------------------------------------------------------------------------------------------------------------------------
566
+ module Log
567
+ Console::Command.pass_through({
568
+ [:_r, :__revision] => 1, # :todo: support "{" DATE "}" format
569
+ [:_q, :__quiet] => 0,
570
+ [:_v, :__verbose] => 0,
571
+ [:__targets] => 1,
572
+ [:__stop_on_copy] => 0,
573
+ [:__incremental] => 0,
574
+ [:__xml] => 0,
575
+ [:__limit] => 1,
576
+ }.merge(SvnCommand::C_standard_remote_command_options), self
577
+ )
578
+ end
579
+ def log(*args)
580
+ puts Subversion.log( prepare_args(args) )
581
+ #svn :exec, *args
582
+ end
583
+
584
+ # Ideas:
585
+ # Just pass a number (5) and it will be treated as --limit 5 (unless File.exists?('5'))
586
+
587
+ #-----------------------------------------------------------------------------------------------------------------------------
588
+ module Mkdir
589
+ Console::Command.pass_through({
590
+ [:_q, :__quiet] => 0,
591
+ }.
592
+ merge(SvnCommand::C_standard_remote_command_options).
593
+ merge(SvnCommand::C_standard_commitable_command_options), self
594
+ )
595
+
596
+ # Make parent directories as needed. (Like the --parents option of GNU mkdir.)
597
+ def __parents; @create_parents = true; end
598
+ alias_method :_p, :__parents
599
+ end
600
+
601
+ def mkdir(*directories)
602
+ if @create_parents
603
+ directories.each do |directory|
604
+
605
+ # :todo: change this so that it's guaranteed to have an exit condition; currently, can get into infinite loop
606
+ loop do
607
+ puts "Creating '#{directory}'"
608
+ FileUtils.mkdir_p directory # Create it if it doesn't already exist
609
+ if Subversion.under_version_control?(File.dirname(directory))
610
+ # Yay, we found a working copy. Now we can issue an add command, from that directory, which will recursively add the
611
+ # (non-working copy) directories we've been creating along the way.
612
+
613
+ #puts Subversion.add prepare_args([directory])
614
+ svn :system, 'add', *directory
615
+ break
616
+ else
617
+ directory = File.dirname(directory)
618
+ end
619
+ end
620
+ end
621
+ else
622
+ # Preserve default behavior.
623
+ svn :system, 'mkdir', *directories
624
+ end
625
+ end
626
+
627
+ #-----------------------------------------------------------------------------------------------------------------------------
628
+ module Move
629
+ Console::Command.pass_through({
630
+ [:_r, :__revision] => 1, # :todo: support "{" DATE "}" format
631
+ [:_q, :__quiet] => 0,
632
+ [:__force] => 0,
633
+ }.
634
+ merge(SvnCommand::C_standard_remote_command_options).
635
+ merge(SvnCommand::C_standard_commitable_command_options), self
636
+ )
637
+
638
+ # If the directory specified by the destination path does not exist, it will `svn mkdir --parents` the directory for you to
639
+ # save you the trouble (and to save you from getting an error message!).
640
+ #
641
+ # For example, if you try to move file_name to dir1/dir2/new_file_name and dir1/dir2 is not under version control, then it
642
+ # will effectively do these commands:
643
+ # svn mkdir --parents dir1/dir2
644
+ # svn mv a dir1/dir2/new_file_name # The command you were originally trying to do
645
+ def __parents; @create_parents = true; end
646
+ alias_method :_p, :__parents
647
+ end
648
+
649
+ def move(*args)
650
+ destination = args.pop
651
+
652
+ # If the last character is a '/', then they obviously expect the destination to be a *directory*. Yet when I do this:
653
+ # svn mv a b/
654
+ # and b doesn't exist,
655
+ # it moves a (a file) to b as a file, rather than creating directory b/ and moving a to b/a.
656
+ # I find this default behavior less than intuitive, so I have "fixed" it here...
657
+ # So instead of seeing this:
658
+ # A b
659
+ # D a
660
+ # You should see this:
661
+ # A b
662
+ # A b/a
663
+ # D a
664
+ if destination[-1..-1] == '/'
665
+ if !File.exist?(destination[0..-2])
666
+ puts "Notice: It appears that the '" + destination.bold + "' directory doesn't exist. Would you like to create it now? Good..."
667
+ self.mkdir destination # @create_parents flag will be reused there
668
+ elsif !File.directory?(destination[0..-2])
669
+ puts "Error".red.bold + ": It appears that '" + destination.bold + "' already exists but is not actually a directory. " +
670
+ "The " + 'destination'.bold + " must either be the path to a " + 'file'.underline + " that does " + 'not'.underline + " yet exist or the path to a " + 'directory'.underline + " (which may or may not yet exist)."
671
+ return
672
+ end
673
+ end
674
+
675
+ if @create_parents and !Subversion.under_version_control?(destination_dir = File.dirname(destination))
676
+ puts "Creating parent directory '#{destination_dir}'..."
677
+ self.mkdir destination_dir # @create_parents flag will be reused there
678
+ end
679
+
680
+ # Unlike the built-in move, this one lets you list multiple source files
681
+ # Source... DestinationDir
682
+ # or
683
+ # Source Destination
684
+ # Useful when you have a long list of files you want to move, such as when you are using wild-cards. Makes commands like this possible:
685
+ # svn mv source/* dest/
686
+ if args.length >= 2
687
+ sources = args
688
+
689
+ sources.each do |source|
690
+ puts filtered_svn('move', source, destination)
691
+ end
692
+ else
693
+ svn :exec, 'move', *(args + [destination])
694
+ end
695
+ end
696
+ alias_subcommand :mv => :move
697
+
698
+ #-----------------------------------------------------------------------------------------------------------------------------
699
+ module Copy
700
+ Console::Command.pass_through({
701
+ [:_r, :__revision] => 1, # :todo: support "{" DATE "}" format
702
+ [:_q, :__quiet] => 0,
703
+ [:__force] => 0,
704
+ }.
705
+ merge(SvnCommand::C_standard_remote_command_options).
706
+ merge(SvnCommand::C_standard_commitable_command_options), self
707
+ )
708
+ end
709
+
710
+ def copy(*args)
711
+ destination = args.pop
712
+
713
+ # Unlike the built-in copy, this one lets you list multiple source files
714
+ # Source... DestinationDir
715
+ # or
716
+ # Source Destination
717
+ # Useful when you have a long list of files you want to copy, such as when you are using wild-cards. Makes commands like this possible:
718
+ # svn cp source/* dest/
719
+ if args.length >= 2
720
+ sources = args
721
+
722
+ sources.each do |source|
723
+ puts filtered_svn('copy', source, destination)
724
+ end
725
+ else
726
+ svn :exec, 'copy', *(args + [destination])
727
+ end
728
+ end
729
+ alias_subcommand :cp => :copy
730
+
731
+ #-----------------------------------------------------------------------------------------------------------------------------
732
+ module Import
733
+ Console::Command.pass_through({
734
+ [:_N, :__non_recursive] => 0,
735
+ [:_q, :__quiet] => 0,
736
+ [:__auto_props] => 0,
737
+ [:__no_auto_props] => 0,
738
+ }.
739
+ merge(SvnCommand::C_standard_remote_command_options).
740
+ merge(SvnCommand::C_standard_commitable_command_options), self
741
+ )
742
+ end
743
+ def import(*args)
744
+ p args
745
+ svn :exec, 'import', *(args)
746
+ end
747
+
748
+ #-----------------------------------------------------------------------------------------------------------------------------
749
+ module Status
750
+ Console::Command.pass_through({
751
+ [:_u, :__show_updates] => 0,
752
+ [:_v, :__verbose] => 0,
753
+ [:_N, :__non_recursive] => 0,
754
+ [:_q, :__quiet] => 0,
755
+ [:__no_ignore] => 0,
756
+ [:__incremental] => 0,
757
+ [:__xml] => 0,
758
+ [:__ignore_externals] => 0,
759
+ }.merge(SvnCommand::C_standard_remote_command_options), self
760
+ )
761
+ end
762
+ def status(*args)
763
+ print Subversion.status_lines_filter( Subversion.status(*(prepare_args(args))) )
764
+ end
765
+
766
+ #-----------------------------------------------------------------------------------------------------------------------------
767
+ module Update
768
+ Console::Command.pass_through({
769
+ [:_r, :__revision] => 1, # :todo: support "{" DATE "}" format
770
+ [:_N, :__non_recursive] => 0,
771
+ [:_q, :__quiet] => 0,
772
+ [:__diff3_cmd] => 1,
773
+ #[:__ignore_externals] => 0,
774
+ }.merge(SvnCommand::C_standard_remote_command_options), self
775
+ )
776
+
777
+ def __ignore_externals; @ignore_externals = true; end
778
+ def __include_externals; @ignore_externals = false; end
779
+ def __with_externals; @ignore_externals = false; end
780
+ alias_method :_ie, :__ignore_externals
781
+ alias_method :_skip_externals, :__ignore_externals
782
+
783
+ def ignore_externals?
784
+ @ignore_externals.nonnil? ?
785
+ @ignore_externals :
786
+ (user_preferences['update'] && user_preferences['update']['ignore_externals'])
787
+ end
788
+
789
+ # Duplicated with Diff
790
+ def __non_recursive
791
+ @non_recursive = true
792
+ @passthrough_options << '--non-recursive'
793
+ end
794
+ alias_method :_N, :__non_recursive
795
+
796
+ end
797
+
798
+ def update(*args)
799
+ @passthrough_options << '--ignore-externals' if ignore_externals?
800
+ Subversion.print_commands! do # Print the commands and options used so they can be reminded that they're using user_preferences['update']['ignore_externals']...
801
+ puts Subversion.update_lines_filter( Subversion.update(*prepare_args(args)) )
802
+ end
803
+ end
804
+
805
+
806
+
807
+
808
+
809
+
810
+
811
+
812
+
813
+ #-----------------------------------------------------------------------------------------------------------------------------
814
+ # Custom subcommands
815
+ #-----------------------------------------------------------------------------------------------------------------------------
816
+
817
+ #-----------------------------------------------------------------------------------------------------------------------------
818
+ def url(*args)
819
+ puts Subversion.url(*args)
820
+ end
821
+
822
+ #-----------------------------------------------------------------------------------------------------------------------------
823
+
824
+ def under_version_control(*args)
825
+ puts Subversion.under_version_control?(*args)
826
+ end
827
+ alias_subcommand :under_version_control? => :under_version_control
828
+
829
+ # Returns root/base *path* for a working copy
830
+ def working_copy_root(*args)
831
+ puts Subversion.working_copy_root(*args)
832
+ end
833
+ alias_subcommand :root => :working_copy_root
834
+
835
+ # Returns the UUID for a working copy/URL
836
+ def repository_uuid(*args)
837
+ puts Subversion.repository_uuid(*args)
838
+ end
839
+ alias_subcommand :uuid => :repository_uuid
840
+
841
+ # Returns root repository *URL* for a working copy
842
+ def repository_root(*args)
843
+ puts Subversion.repository_root(*args)
844
+ end
845
+ alias_subcommand :base_url => :repository_root
846
+ alias_subcommand :root_url => :repository_root
847
+
848
+ #-----------------------------------------------------------------------------------------------------------------------------
849
+ def latest_revision(*args)
850
+ puts Subversion.latest_revision
851
+ end
852
+ alias_subcommand :last_revision => :latest_revision
853
+ alias_subcommand :head => :latest_revision
854
+
855
+ #-----------------------------------------------------------------------------------------------------------------------------
856
+
857
+ # *Experimental*
858
+ #
859
+ # Combine commit messages / diffs for the given range for the given files. Gives one aggregate diff for the range instead of many individual diffs.
860
+ #
861
+ # Could be useful for code reviews?
862
+ #
863
+ # Pass in a list of revisions/revision ranges ("134", "134:136", "134-136", and "134-136 139" are all valid)
864
+ #
865
+ module ViewCommits
866
+ def _r(*revisions)
867
+ # This is necessary so that the -r option doesn't accidentally eat up an arg that wasn't meant to be a revision (a filename, for instance). The only problem with this is if there's actully a filename that matches these patterns! (But then we could just re-order ars.)
868
+ revisions.select! do |revision|
869
+ revision =~ /\d+|\d+:\d+/
870
+ end
871
+ @revisions = revisions
872
+ @revisions.size
873
+ end
874
+ end
875
+ def view_commits(path = "./")
876
+ if @revisions.nil?
877
+ raise "-r (revisions) option is mandatory"
878
+ end
879
+ $ignore_dry_run_option = true
880
+ base_url = Subversion.base_url(path)
881
+ $ignore_dry_run_option = false
882
+ #puts "Base URL: #{base_url}"
883
+ revisions = self.class.parse_revision_ranges(@revisions)
884
+ revisions.each do |revision|
885
+ puts Subversion.log("-r #{revision} -v #{base_url}")
886
+ end
887
+
888
+ puts Subversion.diff("-r #{revisions.first}:#{revisions.last} #{path}")
889
+ #/usr/bin/svn diff http://code.qualitysmith.com/gemables/subversion@2279 http://code.qualitysmith.com/gemables/subwrap@2349 --diff-cmd colordiff
890
+
891
+ end
892
+ alias_subcommand :code_review => :view_commits
893
+
894
+ def SvnCommand.parse_revision_ranges(revisions_array)
895
+ revisions_array.map do |item|
896
+ case item
897
+ when /(\d+):(\d+)/
898
+ ($1.to_i .. $2.to_i)
899
+ when /(\d+)-(\d+)/
900
+ ($1.to_i .. $2.to_i)
901
+ when /(\d+)\.\.(\d+)/
902
+ ($1.to_i .. $2.to_i)
903
+ when /\d+/
904
+ item.to_i
905
+ else
906
+ raise "Item in revisions_array had an unrecognized format: #{item}"
907
+ end
908
+ end.expand_ranges
909
+ end
910
+
911
+ #-----------------------------------------------------------------------------------------------------------------------------
912
+ # Goes through each "unadded" file (each file reporting a status of <tt>?</tt>) reported by <tt>svn status</tt> and asks you what you want to do with them (add, delete, or ignore)
913
+ def each_unadded(*args)
914
+ catch :exit do
915
+
916
+ $ignore_dry_run_option = true
917
+ Subversion.each_unadded( Subversion.status(*args) ) do |file|
918
+ $ignore_dry_run_option = false
919
+ begin
920
+ puts( ('-'*100).green )
921
+ puts "What do you want to do with '#{file.white.underline}'?".white.bold
922
+ begin
923
+ if !File.exist?(file)
924
+ raise "#{file} doesn't seem to exist -- even though it was reported by svn status"
925
+ end
926
+ if File.file?(file)
927
+ if FileTest.binary_file?(file)
928
+ puts "(Binary file -- cannot show preview)".bold
929
+ else
930
+ puts "File contents:"
931
+ # Only show the first x bytes so that we don't accidentally dump the contens of some 20 GB log file to screen...
932
+ contents = File.read(file, bytes_threshold = 5000) || ''
933
+ max_lines = 55
934
+ contents.lines[0..max_lines].each {|line| puts line}
935
+ puts "..." if contents.length >= bytes_threshold # So they know that there may be *more* to the file than what's shown
936
+ end
937
+ elsif File.directory?(file)
938
+ puts "Directory contains:"
939
+ Dir.new(file).reject {|f| ['.','..'].include? f}.each do |f|
940
+ puts f
941
+ end
942
+ else
943
+ raise "#{file} is not a file or directory -- what *is* it then???"
944
+ end
945
+ end
946
+ print(
947
+ "Add".menu_item(:green) + ", " +
948
+ "Delete".menu_item(:red) + ", " +
949
+ "add to " + "svn:".yellow + "Ignore".menu_item(:yellow) + " property, " +
950
+ "ignore ".yellow + "Contents".menu_item(:yellow) + " of directory, " +
951
+ "or " + "any other key".white.bold + " to do nothing > "
952
+ )
953
+ response = ""
954
+ response = $stdin.getch.downcase # while !['a', 'd', 'i', "\n"].include?(begin response.downcase!; response end)
955
+
956
+ case response
957
+ when 'a'
958
+ print "\nAdding... "
959
+ Subversion.add file
960
+ puts
961
+ when 'd'
962
+ puts
963
+
964
+ response = ""
965
+ if File.directory?(file)
966
+ response = confirm("Are you pretty much " + "SURE".bold + " you want to '" + "rm -rf #{file}".red.bold + "'? ")
967
+ else
968
+ response = "y"
969
+ end
970
+
971
+ if response == 'y'
972
+ print "\nDeleting... "
973
+ FileUtils.rm_rf file
974
+ puts
975
+ else
976
+ puts "\nI figured as much!"
977
+ end
978
+ when 'i'
979
+ print "\nIgnoring... "
980
+ Subversion.ignore file
981
+ puts
982
+ else
983
+ # Skip / Do nothing with this file
984
+ puts " (Skipping...)"
985
+ end
986
+ rescue Interrupt
987
+ puts "\nGoodbye!"
988
+ throw :exit
989
+ end
990
+ end # each_unadded
991
+
992
+ end # catch :exit
993
+ end
994
+ alias_subcommand :eu => :each_unadded
995
+ alias_subcommand :unadded => :each_unadded
996
+
997
+
998
+
999
+
1000
+ #-----------------------------------------------------------------------------------------------------------------------------
1001
+ # Externals-related commands
1002
+
1003
+ # Prints out all the externals *items* for the given directory. These are the actual externals listed in an svn:externals property.
1004
+ # Example:
1005
+ # vendor/a
1006
+ # vendor/b
1007
+ # Where 'vendor' is an ExternalsContainer containing external items 'a' and 'b'.
1008
+ # (Use the -o/--omit-repository-path option if you just want the external paths/names without the repository paths)
1009
+ module ExternalsItems
1010
+ def __omit_repository_path
1011
+ @omit_repository_path = true
1012
+ end
1013
+ alias_method :__omit_repository, :__omit_repository_path
1014
+ alias_method :_o, :__omit_repository_path
1015
+ alias_method :_name_only, :__omit_repository_path
1016
+ end
1017
+ def externals_items(directory = "./")
1018
+ longest_path_name = 25
1019
+
1020
+ externals_structs = Subversion.externals_containers(directory).map do |external|
1021
+ returning(
1022
+ external.entries_structs.map do |entry|
1023
+ Struct.new(:path, :repository_path).new(
1024
+ File.join(external.container_dir, entry.name).relativize_path,
1025
+ entry.repository_path
1026
+ )
1027
+ end
1028
+ ) do |entries_structs|
1029
+ longest_path_name =
1030
+ [
1031
+ longest_path_name,
1032
+ entries_structs.map { |entry|
1033
+ entry.path.size
1034
+ }.max.to_i
1035
+ ].max
1036
+ end
1037
+ end
1038
+
1039
+ puts externals_structs.map { |entries_structs|
1040
+ entries_structs.map { |entry|
1041
+ entry.path.ljust(longest_path_name + 1) +
1042
+ (@omit_repository_path ? '' : entry.repository_path)
1043
+ }
1044
+ }
1045
+ puts "(Tip: Also consider using svn externals_outline. Or use the -o/--omit-repository-path option if you just want a list of the paths that are externalled (without the repository URLs that they come from)".magenta unless @omit_repository_path
1046
+ end
1047
+ alias_subcommand :ei => :externals_items
1048
+ alias_subcommand :externals_list => :externals_items
1049
+ alias_subcommand :el => :externals_items
1050
+ alias_subcommand :externals => :externals_items
1051
+ alias_subcommand :e => :externals_items
1052
+
1053
+
1054
+ # For every directory that has the svn:externals property set, this prints out the container name and then lists the contents of its svn:externals property (dir, URL) as a bulleted list
1055
+ def externals_outline(directory = "./")
1056
+ puts Subversion.externals_containers(directory).map { |external|
1057
+ external.to_s.relativize_path
1058
+ }
1059
+ end
1060
+ alias_subcommand :e_outline => :externals_outline
1061
+ alias_subcommand :eo => :externals_outline
1062
+
1063
+ # Lists *directories* that have the svn:externals property set.
1064
+ def externals_containers(directory = "./")
1065
+ puts Subversion.externals_containers(directory).map { |external|
1066
+ external.container_dir
1067
+ }
1068
+ end
1069
+ alias_subcommand :e_containers => :externals_containers
1070
+
1071
+ def edit_externals(directory = nil)
1072
+ catch :exit do
1073
+ if directory.nil? || !Subversion::ExternalsContainer.new(directory).has_entries?
1074
+ if directory.nil?
1075
+ puts "No directory specified. Editing externals for *all* externals dirs..."
1076
+ directory = "./"
1077
+ else
1078
+ puts "Editing externals for *all* externals dirs..."
1079
+ end
1080
+ Subversion.externals_containers(directory).each do |external|
1081
+ puts external.to_s
1082
+ command = "propedit svn:externals #{external.container_dir}"
1083
+ begin
1084
+ response = confirm("Do you want to edit svn:externals for this directory?".black_on_white)
1085
+ svn :system, command if response == 'y'
1086
+ rescue Interrupt
1087
+ puts "\nGoodbye!"
1088
+ throw :exit
1089
+ ensure
1090
+ puts
1091
+ end
1092
+ end
1093
+ puts 'Done'
1094
+ else
1095
+ #system "#{Subversion.executable} propedit svn:externals #{directory}"
1096
+ svn :system, "propedit svn:externals #{directory}"
1097
+ end
1098
+ end # catch :exit
1099
+ end
1100
+ alias_subcommand :edit_ext => :edit_externals
1101
+ alias_subcommand :ee => :edit_externals
1102
+ alias_subcommand :edit_external => :edit_externals
1103
+
1104
+ module Externalize
1105
+ # :todo: shortcut to create both __whatever method that sets instance variable
1106
+ # *and* accessor method 'whatever' for reading it (and ||= initializing it)
1107
+ # Console::Command.option({
1108
+ # :as => 1 # 1 is arity
1109
+ # :as => [1, nil] # 1 is arity, nil is default?
1110
+ # )
1111
+
1112
+ def __as(as); @as = as; end
1113
+ def as; @as; end
1114
+ end
1115
+ # svn externalize http://your/repo/shared_tasks/tasks --as shared
1116
+ # or
1117
+ # svn externalize http://your/repo/shared_tasks/tasks shared
1118
+ def externalize(repo_path, as_arg = nil)
1119
+ # :todo: let them pass in local_path as well? -- then we would need to accept 2 -- 3 -- args, the first one poylmorphic, the second optional
1120
+ # :todo: automated test for as_arg/as combo
1121
+
1122
+ Subversion.externalize(repo_path, {:as => as || as_arg})
1123
+ end
1124
+
1125
+
1126
+ #-----------------------------------------------------------------------------------------------------------------------------
1127
+
1128
+ def ignore(file)
1129
+ Subversion.ignore(file)
1130
+ end
1131
+
1132
+ # Example:
1133
+ # svn edit_ignores tmp/sessions/
1134
+ def edit_ignores(directory = './')
1135
+ #puts Subversion.get_property("ignore", directory)
1136
+ # If it's empty, ask them if they want to edit it anyway??
1137
+
1138
+ svn :system, "propedit svn:ignore #{directory}"
1139
+ end
1140
+
1141
+ #-----------------------------------------------------------------------------------------------------------------------------
1142
+ # Commit message retrieving/editing
1143
+
1144
+ module GetMessage
1145
+ def _r(revision)
1146
+ @revision = revision
1147
+ end
1148
+ end
1149
+ def get_message()
1150
+ #svn propget --revprop svn:log -r2325
1151
+ args = ['propget', '--revprop', 'svn:log']
1152
+ #args.concat ['-r', @revision ? @revision : Subversion.latest_revision]
1153
+ args.concat ['-r', (revision = @revision ? @revision : 'head')]
1154
+ puts "Message for r#{Subversion.latest_revision} :" if revision == 'head'
1155
+
1156
+ $ignore_dry_run_option = true
1157
+ puts filtered_svn(*args)
1158
+ $ignore_dry_run_option = false
1159
+ end
1160
+
1161
+ module SetMessage
1162
+ def _r(revision)
1163
+ @revision = revision
1164
+ end
1165
+ def __file(filename)
1166
+ @filename = filename
1167
+ end
1168
+ end
1169
+ def set_message(new_message)
1170
+ #svn propset --revprop -r 25 svn:log "Journaled about trip to New York."
1171
+ puts "Message before changing:"
1172
+ get_message
1173
+
1174
+ args = ['propset', '--revprop', 'svn:log']
1175
+ args.concat ['-r', @revision ? @revision : 'head']
1176
+ args << new_message if new_message
1177
+ if @filename
1178
+ contents = File.readlines(@filename).join.strip
1179
+ puts "Read file '#{@filename}':"
1180
+ print contents
1181
+ puts
1182
+ args << contents
1183
+ end
1184
+ svn :exec, *args
1185
+ end
1186
+
1187
+ # Lets you edit it with your default editor
1188
+ module EditRevisionProperty
1189
+ def _r(revision)
1190
+ @revision = revision
1191
+ end
1192
+ end
1193
+ def edit_revision_property(property_name, directory = './')
1194
+ args = ['propedit', '--revprop', property_name, directory]
1195
+ rev = @revision ? @revision : 'head'
1196
+ args.concat ['-r', rev]
1197
+ Subversion.print_commands! do
1198
+ svn :system, *args
1199
+ end
1200
+
1201
+ value = Subversion::get_revision_property(property_name, rev)
1202
+ p value
1203
+
1204
+ # Currently there is no seperate option to *delete* a revision property (propdel)... That would be useful for those
1205
+ # properties that are just boolean *flags* (set or not set).
1206
+ # I'm assuming most people will very rarely if ever actually want to set a property to the empty string (''), so
1207
+ # we can use the empty string as a way to trigger a propdel...
1208
+ if value == ''
1209
+ puts
1210
+ response = confirm("Are you sure you want to delete property #{property_name}".red.bold + "'? ")
1211
+ puts
1212
+ if response == 'y'
1213
+ Subversion.print_commands! do
1214
+ Subversion::delete_revision_property(property_name, rev)
1215
+ end
1216
+ end
1217
+ end
1218
+ end
1219
+
1220
+ # Lets you edit it with your default editor
1221
+ module EditMessage
1222
+ def _r(revision)
1223
+ @revision = revision
1224
+ end
1225
+ end
1226
+ def edit_message(directory = './')
1227
+ edit_revision_property('svn:log', directory)
1228
+ end
1229
+
1230
+ def edit_property(property_name, directory = './')
1231
+ end
1232
+
1233
+ #-----------------------------------------------------------------------------------------------------------------------------
1234
+ # Cause a working copy to cease being a working copy
1235
+ def delete_svn(directory = './')
1236
+ puts "If you continue, all of the following directories/files will be deleted:"
1237
+ system("find #{directory} -name .svn | xargs -n1 echo")
1238
+ response = confirm("Do you wish to continue?")
1239
+ puts
1240
+ if response == 'y'
1241
+ system("find #{directory} -name .svn | xargs -n1 rm -r")
1242
+ end
1243
+ end
1244
+
1245
+ #-----------------------------------------------------------------------------------------------------------------------------
1246
+
1247
+ def add_all_unadded
1248
+ raise NotImplementedError
1249
+ end
1250
+ def grep
1251
+ raise NotImplementedError
1252
+ end
1253
+ def grep_externals
1254
+ raise NotImplementedError
1255
+ end
1256
+ def grep_log
1257
+ raise NotImplementedError
1258
+ end
1259
+
1260
+ #-----------------------------------------------------------------------------------------------------------------------------
1261
+ module Revisions
1262
+ # Start at earlier revision and go forwards rather than starting at the latest revision
1263
+ #def __reverse
1264
+ def __forward
1265
+ @reverse = true
1266
+ end
1267
+ def __forwards
1268
+ @reverse = true
1269
+ end
1270
+
1271
+ # Only show revisions that are in need of a code review
1272
+ # :todo:
1273
+ def __unreviewed_only
1274
+ @unreviewed_only = true
1275
+ end
1276
+
1277
+ # Only show revisions that were committed by a certain author.
1278
+ # :todo:
1279
+ def __by(author)
1280
+ @author_filter = author
1281
+ end
1282
+ def __author(author)
1283
+ @author_filter = author
1284
+ end
1285
+ end
1286
+ def revisions(directory = './')
1287
+ puts "Getting list of revisions for '#{directory.white.bold}' ..."
1288
+
1289
+ head = Subversion.latest_revision
1290
+ revision_of_directory = Subversion.latest_revision_for_path(directory)
1291
+
1292
+ # It's possible for a working copy to get "out of date" (even if you were the last committer!), in which case svn log will
1293
+ # only list revisions up to that revision (actually looks like it only goes up to and including Last Changed Rev: 2838,
1294
+ # not Revision: 2839, as reported by svn info...)
1295
+ if revision_of_directory and head and revision_of_directory < head
1296
+ puts "The working copy '#{directory.white.bold}' appears to be out-of-date (#{revision_of_directory}) with respect to the head revision (#{head}). Updating..."
1297
+ Subversion.update(directory)
1298
+ end
1299
+
1300
+ revisions = Subversion.revisions(directory)
1301
+
1302
+ puts "#{revisions.length.to_s.bold} revisions found. Starting with #{@reverse ? 'oldest' : 'most recent'} revision and #{@reverse ? 'going forward in time' : 'going backward in time'}..."
1303
+ revisions.instance_variable_get(:@revisions).reverse! if @reverse
1304
+ revision_ids = revisions.map(&:identifier)
1305
+
1306
+ target_rev = nil # revision_ids.first
1307
+ show_revision_again = true
1308
+ revisions.each do |revision|
1309
+ rev = revision.identifier
1310
+ other_rev = rev-1
1311
+ if target_rev
1312
+ if rev == target_rev
1313
+ target_rev = nil # We have arrived.
1314
+ else
1315
+ next # Keep going (hopefully in the right direction!)
1316
+ end
1317
+ end
1318
+
1319
+ # Display the revision
1320
+ if show_revision_again
1321
+ puts((' '*100).green.underline)
1322
+ puts "#{revisions.length - revision_ids.index(rev)}. ".green.bold +
1323
+ "r#{rev}".magenta.bold + (rev == head ? ' (head)'.bold : '') +
1324
+ " | #{revision.developer} | #{revision.time.strftime('%Y-%m-%d %H:%M:%S')}".magenta.bold
1325
+ puts revision.message
1326
+ puts
1327
+ #pp revision
1328
+ puts revision.map {|a|
1329
+ (a.status ? a.status[0..0].colorize_svn_status_code : ' ') + # This check is necessary because RSCM doesn't recognize several Subversion status flags, including 'R', and status will return nil in these cases.
1330
+ ' ' + a.path
1331
+ }.join("\n")
1332
+ else
1333
+ show_revision_again = true
1334
+ end
1335
+
1336
+ # Display the menu
1337
+ print(
1338
+ "r#{rev}".magenta.on_blue.bold + ': ' +
1339
+ 'View this changeset'.menu_item(:cyan) + ', ' +
1340
+ 'Diff against specific revision'.menu_item(:cyan, 'D') + ', ' +
1341
+ 'Grep the changeset'.menu_item(:cyan, 'G') + ', ' +
1342
+ 'List or '.menu_item(:magenta, 'L') + '' +
1343
+ 'Edit revision properties'.menu_item(:magenta, 'E') + ', ' +
1344
+ 'svn Cat all files'.menu_item(:cyan, 'C') + ', ' +
1345
+ 'grep the cat'.menu_item(:cyan, 'a') + ', ' + "\n " +
1346
+ 'mark as Reviewed'.menu_item(:green, 'R') + ', ' +
1347
+ 'edit log Message'.menu_item(:yellow, 'M') + ', ' +
1348
+ 'or ' + 'browse using ' + 'Up/Down/Enter'.white.bold + ' keys > '
1349
+ )
1350
+
1351
+ # Get response from user and then act on it
1352
+ begin # rescue
1353
+ response = ""
1354
+ response = $stdin.getch.downcase
1355
+
1356
+ # Escape sequence such as the up arrow key ("\e[A")
1357
+ if response == "\e"
1358
+ response << (next_char = $stdin.getch)
1359
+ if next_char == '['
1360
+ response << (next_char = $stdin.getch)
1361
+ end
1362
+ end
1363
+
1364
+ if response == 'd' # diff against Other revision
1365
+ response = 'v'
1366
+ puts
1367
+ print 'All right, which revision shall it be then? '.bold + ' (backspace not currently supported)? '
1368
+ other_rev = $stdin.gets.chomp.to_i
1369
+ end
1370
+
1371
+ case response
1372
+ when 'v' # View this changeset
1373
+ revs_to_compare = [other_rev, rev]
1374
+ puts "\n"*10
1375
+ puts((' '*100).green.underline)
1376
+ print "Diffing #{revs_to_compare.min}:#{revs_to_compare.max}... ".bold
1377
+ puts
1378
+ #Subversion.repository_root
1379
+ Subversion.print_commands! do
1380
+ SvnCommand.execute("diff #{directory} --ignore-externals -r #{revs_to_compare.min}:#{revs_to_compare.max}")
1381
+ end
1382
+ show_revision_again = false
1383
+
1384
+ when 'g' # Grep the changeset
1385
+ # :todo; make it accept regexpes like /like.*this/im so you can make it case insensitive or multi-line
1386
+ revs_to_compare = [other_rev, rev]
1387
+ puts
1388
+ print 'Grep for'.bold + ' (Case sensitive; Regular expressions ' + 'like.*this'.bold.blue + ' allowed, but not ' + '/like.*this/im'.bold.blue + ') (backspace not currently supported): '
1389
+ search_pattern = $stdin.gets.chomp.to_rx
1390
+ puts((' '*100).green.underline)
1391
+ puts "Searching `svn diff #{directory} -r #{revs_to_compare.min}:#{revs_to_compare.max}` for #{search_pattern.to_s}... ".bold
1392
+ diffs = nil
1393
+ Subversion.print_commands! do
1394
+ diffs = Subversion.diffs(directory, '-r', "#{revs_to_compare.min}:#{revs_to_compare.max}")
1395
+ end
1396
+
1397
+ hits = 0
1398
+ diffs.each do |filename, diff|
1399
+ #.grep(search_pattern)
1400
+ if diff.diff =~ search_pattern
1401
+ puts diff.filename_pretty
1402
+ puts( diff.diff.grep(search_pattern). # This will get us just the interesting *lines* (as an array).
1403
+ map { |line| # Now, for each line...
1404
+ hits += 1
1405
+ line.highlight_occurences(search_pattern)
1406
+ }
1407
+ )
1408
+ end
1409
+ end
1410
+ if hits == 0
1411
+ puts "Search term not found!".red.bold
1412
+ end
1413
+ show_revision_again = false
1414
+
1415
+ when 'a' # Grep the cat
1416
+ puts
1417
+ print 'Grep for'.bold + ' (Case sensitive; Regular expressions ' + 'like.*this'.bold.blue + ' allowed, but not ' + '/like.*this/im'.bold.blue + ') (backspace not currently supported): '
1418
+ search_pattern = $stdin.gets.chomp.to_rx
1419
+ puts((' '*100).green.underline)
1420
+ puts "Searching `svn cat #{directory} -r #{rev}` for #{search_pattern.to_s}... ".bold
1421
+ contents = nil
1422
+ Subversion.print_commands! do
1423
+ contents = Subversion.cat(directory, '-r', rev)
1424
+ end
1425
+
1426
+ if contents =~ search_pattern
1427
+ puts( contents.grep(search_pattern). # This will get us just the interesting *lines* (as an array).
1428
+ map { |line| # Now, for each line...
1429
+ line.highlight_occurences(search_pattern)
1430
+ }
1431
+ )
1432
+ else
1433
+ puts "Search term not found!".red.bold
1434
+ end
1435
+ show_revision_again = false
1436
+
1437
+
1438
+ when 'l' # List revision properties
1439
+ puts
1440
+ puts Subversion::printable_revision_properties(rev)
1441
+ show_revision_again = false
1442
+
1443
+ when 'e' # Edit revision property
1444
+ puts
1445
+ puts Subversion::printable_revision_properties(rev)
1446
+ puts "Warning: These properties are *not* under version control! Try not to permanently destroy anything *too* important...".red.bold
1447
+ puts "Note: If you want to *delete* a property, simply set its value to '' and it will be deleted (propdel) for you."
1448
+ print 'Which property would you like to edit'.bold + ' (backspace not currently supported)? '
1449
+ property_name = $stdin.gets.chomp
1450
+ unless property_name == ''
1451
+ Subversion.print_commands! do
1452
+ @revision = rev
1453
+ edit_revision_property(property_name, directory)
1454
+ end
1455
+ end
1456
+
1457
+ show_revision_again = false
1458
+
1459
+ when 'c' # Cat all files from revision
1460
+ puts
1461
+ Subversion.print_commands! do
1462
+ puts Subversion.cat(directory, '-r', rev)
1463
+ end
1464
+ show_revision_again = true
1465
+
1466
+ when 'r' # Mark as reviewed
1467
+ puts
1468
+ your_name = ENV['USER'] # I would use the same username that Subversion itself would use if you committed
1469
+ # something (since it is sometimes different from your system username), but I don't know
1470
+ # how to retrieve that (except by poking around in your ~/.subversion/ directory, but
1471
+ # that seems kind of rude...).
1472
+ puts "Marking as reviewed by '#{your_name}'..."
1473
+ Subversion.print_commands! do
1474
+ puts svn(:capture, "propset code:reviewed '#{your_name}' --revprop -r #{rev}", :prepare_args => false)
1475
+ # :todo: Maybe *append* to code:reviewed (,-delimited) rather than overwriting it?, in case there is a policy of requiring 2 reviewers or something
1476
+ end
1477
+ show_revision_again = false
1478
+
1479
+ when 'm' # Edit log message
1480
+ puts
1481
+ Subversion.print_commands! do
1482
+ SvnCommand.execute("edit_message -r #{rev}")
1483
+ end
1484
+ show_revision_again = false
1485
+
1486
+ when "\e[A" # Up
1487
+ i = revision_ids.index(rev)
1488
+ target_rev = revision_ids[i - 1]
1489
+ puts " Previous..."
1490
+ retry
1491
+
1492
+
1493
+ when /\n|\e\[B/ # Enter or Down
1494
+ # Skip / Do nothing with this file
1495
+ puts " Next..."
1496
+ next
1497
+
1498
+ else
1499
+ # Invalid option. Do nothing.
1500
+ #puts response.inspect
1501
+ puts
1502
+ show_revision_again = false
1503
+
1504
+ end # case response
1505
+
1506
+ redo # Until they tell us they're ready to move on...
1507
+
1508
+ rescue Interrupt
1509
+ puts "\nGoodbye!"
1510
+ return
1511
+ end # rescue
1512
+ end
1513
+ end
1514
+ alias_subcommand :changesets => :revisions
1515
+ alias_subcommand :browse => :revisions
1516
+ alias_subcommand :browse_log => :revisions
1517
+ alias_subcommand :browse_revisions => :revisions
1518
+ alias_subcommand :browse_changesets => :revisions
1519
+ # See also the implementation of revisions() in /usr/lib/ruby/gems/1.8/gems/rscm-0.5.1/lib/rscm/scm/subversion.rb
1520
+ # Other name ideas: browse, list_commits, changeset_browser, log_browser, interactive_log
1521
+
1522
+ #-----------------------------------------------------------------------------------------------------------------------------
1523
+ # Aliases
1524
+ #:stopdoc:
1525
+ alias_subcommand :st => :status
1526
+ alias_subcommand :up => :update
1527
+ alias_subcommand :ci => :commit
1528
+ #:startdoc:
1529
+
1530
+
1531
+ #-----------------------------------------------------------------------------------------------------------------------------
1532
+ # Helpers
1533
+
1534
+ private
1535
+ def svn(method, *args)
1536
+ subcommand = args[0]
1537
+ options = args.last.is_a?(Hash) ? args.last : {}
1538
+ args = (
1539
+ [subcommand] +
1540
+ (
1541
+ (options[:prepare_args] == false) ?
1542
+ [] :
1543
+ (prepare_args(args[1..-1] || []))
1544
+ ) +
1545
+ [:method => method]
1546
+ )
1547
+ # puts "in svn(): about to call Subversion#execute(#{args.inspect})"
1548
+ Subversion.send :execute, *args
1549
+ end
1550
+
1551
+ # Works identically to svn() except that it filters the output and displays a big red error message if /usr/bin/svn exied with an error.
1552
+ def filtered_svn(*args)
1553
+ # We have to use the :capture method if we're going to filter the output.
1554
+ svn(:capture, *args).add_exit_code_error
1555
+ end
1556
+
1557
+ # Removes nil elements, converts to strings, and adds any pass-through args that may have been provided.
1558
+ def prepare_args(args)
1559
+ args.compact! # nil elements spell trouble
1560
+ args.map!(&:to_s) # shell_escape doesn't like Fixnums either
1561
+ @passthrough_options + args.shell_escape
1562
+ end
1563
+ # To allow testing/stubbing
1564
+ def system(*args)
1565
+ Kernel.system *args
1566
+ end
1567
+ end
1568
+ end