subwrap 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/ProjectInfo.rb +54 -0
- data/Readme +458 -0
- data/bin/_subwrap_post_install +25 -0
- data/bin/command_completion_for_subwrap +21 -0
- data/bin/rscm_test +19 -0
- data/bin/subwrap +7 -0
- data/bin/svn +7 -0
- data/lib/subwrap.rb +6 -0
- data/lib/subwrap/subversion.rb +599 -0
- data/lib/subwrap/subversion_extensions.rb +148 -0
- data/lib/subwrap/svn_command.rb +1568 -0
- data/test/subversion_extensions_test.rb +72 -0
- data/test/subversion_test.rb +132 -0
- data/test/svn_command_test.rb +649 -0
- data/test/test_helper.rb +30 -0
- metadata +120 -0
@@ -0,0 +1,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
|