svn-command 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/Readme ADDED
@@ -0,0 +1,251 @@
1
+ = <i>Enhanced Subversion command</i> -- an +svn+ command wrapper
2
+
3
+ [*Environment*:] Command line
4
+ [<b>Project site</b>:] http://rubyforge.org/projects/svncommand
5
+ [<b>Documentation</b>:] http://svncommand.rubyforge.org/
6
+ [<b>Wiki</b>:] http://wiki.qualitysmith.com/svn-command
7
+ [<b>Author</b>:] Tyler Rick
8
+
9
+ == Introduction
10
+
11
+ This is a replacement <tt>svn</tt> <b>command-line client</b> meant to be used instead of the standard +svn+ command.
12
+
13
+ == Installation
14
+
15
+ === Prerequisites
16
+
17
+ Currently a _patched_ version of Console::Command is required. The patched vesion is available on riblet.qualitysmith.com at:
18
+
19
+ /usr/lib/ruby/gems/1.8/gems/facets-1.8.51/lib/facets/more/command.rb
20
+
21
+ (These changes will hopefully be absorbed into the next release.)
22
+
23
+ === Installation: Per system
24
+
25
+ sudo gem install svn-command
26
+
27
+ You also need to make those files executable (once per _system_):
28
+
29
+ sudo chmod a+x /usr/lib/ruby/gems/1.8/gems/svn-command-0.0.3/bin/*
30
+
31
+ (We can't just set <tt>executables = "svn"</tt> because that would cause it to wipe out the existing executable at <tt>/usr/bin/svn</tt>! If you know of a better, more automatic solution to this, please let the developers know!)
32
+
33
+ And for some reason I seem to have to restart my terminal after doing the chmod step for bash to detect the svn command in that new location.
34
+
35
+ === Installation: Per user
36
+
37
+ *Important*: You need the gem's +bin+ directory to be added to the <b><i>front</i></b> of your path. This requires adding/editing a <tt>PATH=</tt> command in your <tt>~/.bash_profile</tt>. For example:
38
+
39
+ export PATH=/usr/lib/ruby/gems/1.8/gems/svn-command-0.0.3/bin:$PATH
40
+
41
+ (I'm not sure if this is possible to automate with the <tt>gem install</tt> process or not. But in the meantime you need to do it manually.)
42
+
43
+ You'll know it's working by way of two signs:
44
+ * Your +svn+ command will be noticeably slower
45
+ * When you type svn <tt>help</tt>, it will say:
46
+ You are using svn-command, a replacement/wrapper for the standard svn command.
47
+
48
+ == Features
49
+
50
+ Changes to existing subcommands:
51
+ * <tt>svn diff</tt> output is in _color_ (requires +colordiff+, see below)
52
+ * <tt>svn diff</tt> includes the differences from your *externals* too (consistent with how <tt>svn status</tt> includes them) so that you don't forget to commit those changes too!
53
+ * <tt>svn status</tt> output filters out distracting, useless output about externals (if you want a list of externals, use <tt>svn externals</tt>
54
+
55
+ New subcommands:
56
+ * <tt>svn each_unadded</tt> (+eu+) -- goes through each unadded (<tt>?</tt>) file reported by <tt>svn status</tt> and asks you what to do with them (add, delete, ignore).
57
+ * <tt>svn externals</tt>
58
+ * <tt>svn edit_externals</tt> (+ee+)
59
+ * <tt>svn externalize</tt>
60
+ * <tt>svn set_message</tt> / <tt>svn get_message</tt> / <tt>svn edit_message</tt>
61
+ * <tt>svn ignore</tt>
62
+ * <tt>svn view_commits</tt> (gives you output from both svn log and from svn diff for the given changesets)
63
+
64
+ (RDoc question: how do I make the identifiers like Subversion::SvnCommand#externalize into links??)
65
+
66
+ == Usage
67
+
68
+ === <tt>svn st</tt>
69
+
70
+ _Without_ this gem installed (really long):
71
+
72
+ ? gemables/subversion/ruby_subversion.rb
73
+ M gemables/subversion
74
+ M gemables/subversion/lib/subversion.rb
75
+ A gemables/subversion/bin
76
+ A gemables/subversion/bin/svn
77
+ X plugins/database_log4r/tasks/shared
78
+ X plugins/surveys/doc/template
79
+ X plugins/surveys/tasks/shared
80
+ X gemables/dev_scripts/tasks/shared
81
+ X gemables/dev_scripts/lib/subversion
82
+
83
+ Performing status on external item at 'plugins/database_log4r/tasks/shared'
84
+
85
+ Performing status on external item at 'plugins/surveys/tasks/shared'
86
+
87
+ Performing status on external item at 'gemables/subversion/doc_include/template'
88
+
89
+ Performing status on external item at 'gemables/dev_scripts/tasks/shared'
90
+
91
+ Performing status on external item at 'applications/underlord/vendor/plugins/rails_smith'
92
+ X applications/underlord/vendor/plugins/rails_smith/tasks/shared
93
+ X applications/underlord/vendor/plugins/rails_smith/lib/subversion
94
+ X applications/underlord/vendor/plugins/rails_smith/doc_include/template
95
+
96
+ Performing status on external item at 'applications/underlord/vendor/plugins/rails_smith/tasks/shared'
97
+ M applications/underlord/vendor/plugins/rails_smith/tasks/shared/base.rake
98
+
99
+ <b>_With_</b> this gem installed (_much_ shorter and sweeter):
100
+
101
+ ? gemables/subversion/ruby_subversion.rb
102
+ M gemables/subversion
103
+ M gemables/subversion/lib/subversion.rb
104
+ A gemables/subversion/bin
105
+ A gemables/subversion/bin/svn
106
+ M applications/underlord/vendor/plugins/rails_smith/tasks/shared/base.rake
107
+
108
+ === <tt>svn each_unadded</tt>
109
+
110
+ My personal favorite. This command is useful for keeping your working copies clean -- getting rid of all those accumulated temp files (or *ignoring* or *adding* them if they're something that _all_ users of this repository should be aware of).
111
+
112
+ It simply 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*.
113
+
114
+ > svn each_unadded
115
+
116
+ What do you want to do with plugins/database_log4r/doc?
117
+ (shows preview)
118
+ (a)dd, (d)elete, add to svn:(i)ignore property, or [Enter] to do nothing > i
119
+ Ignoring...
120
+
121
+ What do you want to do with applications/underlord/db/schema.rb?
122
+ (shows preview)
123
+ (a)dd, (d)elete, add to svn:(i)ignore property, or [Enter] to do nothing > a
124
+ Adding...
125
+
126
+ What do you want to do with applications/underlord/vendor/plugins/exception_notification?
127
+ (shows preview)
128
+ (a)dd, (d)elete, add to svn:(i)ignore property, or [Enter] to do nothing > d
129
+ Are you pretty much *SURE* you want to 'rm -rf applications/underlord/vendor/plugins/exception_notification'? (y)es, (n)o > y
130
+ Deleting...
131
+
132
+ For *files*, it will show a preview of the _contents_ of that file (limited to the first 3000 characters); for *directories*, it will show a _directory_ _listing_. By looking at the preview, you should hopefully be able to decide whether you want to keep the file or junk it.
133
+
134
+ ===externalize / externals / edit_externals
135
+
136
+ Shortcut for creating an svn:external...
137
+
138
+ your_project/vendor/ > svn externalize http://code.qualitysmith.com/gemables/svn-command --as svn
139
+
140
+ Between that and externals / edit_externals, that's all you ever really need! (?)
141
+
142
+ > svn externals
143
+ /home/tyler/code/plugins/rails_smith/tasks
144
+ * shared http://code.qualitysmith.com/gemables/shared_tasks/tasks
145
+ /home/tyler/code/plugins/rails_smith/doc_include
146
+ * template http://code.qualitysmith.com/gemables/template/doc/template
147
+ /home/tyler/code/plugins/rails_smith
148
+ * svn-command http://code.qualitysmith.com/gemables/svn-command
149
+
150
+ Oops, I externalled it in the wrong place!
151
+
152
+ > svn edit_externals
153
+ /home/tyler/code/plugins/rails_smith/tasks
154
+ * shared http://code.qualitysmith.com/gemables/shared_tasks/tasks
155
+ Do you want to edit svn:externals for this directory? y/N > [Enter]
156
+
157
+ /home/tyler/code/plugins/rails_smith/doc_include
158
+ * template http://code.qualitysmith.com/gemables/template/doc/template
159
+ Do you want to edit svn:externals for this directory? y/N > [Enter]
160
+
161
+ /home/tyler/code/plugins/rails_smith
162
+ * svn-command http://code.qualitysmith.com/gemables/svn-command
163
+ Do you want to edit svn:externals for this directory? y/N > [y]
164
+ (remove that line using your favorite editor (which of course is +vim+), save, quit)
165
+
166
+ You can also pass a directory name to edit_externals to edit the svn:externals property for that directory:
167
+
168
+ > svn edit-externals vendor/plugins
169
+
170
+ ===get_message / set_message / edit_message
171
+
172
+ <b>Pre-requisite for set_message/edit_message</b>: Your repository must have a <tt>pre-revprop-change</tt> hook file.
173
+
174
+ Useful if you made a mistake or forgot something in your commit message and want to edit it...
175
+
176
+ For example, maybe you tried to do a multi-line commit message with -m but it didn't interpret your "\n"s as newline characters. Just run svn edit_message and fix it interactively!
177
+
178
+ svn get_message -r 2325
179
+ is the same as:
180
+ svn propget -r 2325 --revprop svn:log
181
+
182
+ If you *just* committed it and you want to edit the message for the most-recently committed revision ("head"), there is an even quicker way to do it:
183
+
184
+ You can do this:
185
+ svn edit_message -r head
186
+ or just this:
187
+ svn edit_message
188
+
189
+ ===Help
190
+
191
+ You can, of course, get a lits of the custom commands that have been added by using <tt>svn help</tt>. They will be listed at the end.
192
+
193
+ ===Global options
194
+
195
+ * --no-color (since color is on by default)
196
+ * --dry-run (see what /usr/bin/svn command it _would_ have executed if you weren't just doing a dry run -- useful for debugging if nothing else)
197
+ * --debug (sets $debug = true)
198
+
199
+ ==colordiff
200
+
201
+ +colordiff+ is used to colorize <tt>svn diff</tt> commands (+ lines are blue; - lines are red)
202
+
203
+ Found at:
204
+ * http://www.pjhyett.com/articles/2006/06/16/colored-svn-diff
205
+ * http://colordiff.sourceforge.net/
206
+
207
+ Suggestion: change the colors in <tt>/etc/colordiffrc</tt> to be more readable:
208
+ plain=white
209
+ newtext=green
210
+ oldtext=red
211
+ diffstuff=cyan
212
+ cvsstuff=magenta
213
+
214
+ ==Bash command completion
215
+
216
+ If you want command completion for the svn subcommands (and I don't blame you if you don't -- the default command completion is <i>much faster</i> and already gives you completion for filenames!), just add this line to your <tt>~/.bashrc</tt> :
217
+
218
+ complete -C /usr/bin/command_completion_for_svn_command -o default svn
219
+
220
+ It's really rudimentary right now and could be much improved, but at least it's a start.
221
+
222
+ ==Known problems
223
+
224
+ It doesn't support options that are given in this format:
225
+ --diff-cmd=colordiff
226
+ only this format:
227
+ --diff-cmd colordiff
228
+ This is a limitation of Console::Command.
229
+
230
+ === Slowness
231
+
232
+ Is it slower than just running /usr/bin/svn directly? You betcha it is!
233
+
234
+ > time svn foo
235
+ real 0m0.493s
236
+
237
+ > time /usr/bin/svn foo
238
+ real 0m0.019s
239
+
240
+ But... as with most things written in Ruby, it's all more about *productivity* than raw execution speed. _Hopefully_ the productivity gains you get from using this wrapper will more than make up for the 0.5 s extra you have to wait for the svn command. :-) If not, I guess it's not for you.
241
+
242
+ ==To do
243
+
244
+ Take the best ideas from these and incorporate:
245
+ * /usr/lib/ruby/gems/1.8/gems/rscm-0.5.1/lib/rscm/scm/subversion.rb
246
+ * /usr/lib/ruby/gems/1.8/gems/rscm-0.5.1/lib/rscm/scm/subversion_log_parser.rb
247
+ * /usr/lib/ruby/gems/1.8/gems/lazysvn-0.1.3/lib/subversion.rb
248
+
249
+ Possibly switch to LazySvn.
250
+
251
+ http://wiki.qualitysmith.com/svn-command
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'svn_command'
5
+
6
+ subcommands = (Subversion::SvnCommand.public_instance_methods - Object.methods).sort
7
+
8
+ # COMP_LINE will be something like 'svn sta' (what they started to type).
9
+ exit 0 unless /^svn\b/ =~ ENV["COMP_LINE"]
10
+ after_match = $'
11
+
12
+ # Since we only want our custom completion for the first argument passed to svn (because we want it to fall back to using default completion for all subsequent args), we need to check what the COMP_LINE is:
13
+ exit 0 if /^svn\s[\w=-]* / =~ ENV["COMP_LINE"]
14
+ # 'svn --diff-cmd=whatever ' =~ /^svn\b [\w=-]* / => 0
15
+
16
+ subcommand_match = (after_match.empty? || after_match =~ /\s$/) ? nil : after_match.split.last
17
+ subcommands = subcommands.select { |t| /^#{Regexp.escape subcommand_match}/ =~ t } if subcommand_match
18
+
19
+ puts subcommands
20
+ exit 0
data/bin/rscm_test ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rscm'
5
+ require 'pp'
6
+
7
+ #scm = RSCM::Subversion.new("http://code.qualitysmith.com/gemables")
8
+
9
+ subversion = RSCM::Subversion.new()
10
+ subversion.checkout_dir = '.'
11
+ puts "working copy revision = #{subversion.label}"
12
+ puts "url = #{subversion.repourl}"
13
+ puts "up to date? = #{subversion.uptodate?(nil)}"
14
+
15
+
16
+ #revisions = scm.revisions(Time.utc(2007, 01, 10, 12, 34, 22)) # For Subversion, you can also pass a revision number (int)
17
+ #revisions.each do |revision|
18
+ # pp revision
19
+ #end
data/bin/svn ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require_gem 'facets', '>=1.8.20'
5
+ require 'facets/core/kernel/require_local'
6
+ require_local '../lib/svn_command.rb'
7
+ Subversion::SvnCommand.execute
@@ -0,0 +1,44 @@
1
+ # Extends the module object with module and instance accessors for class attributes,
2
+ # just like the native attr* accessors for instance attributes.
3
+ class Module # :nodoc:
4
+ def mattr_reader(*syms)
5
+ syms.each do |sym|
6
+ class_eval(<<-EOS, __FILE__, __LINE__)
7
+ unless defined? @@#{sym}
8
+ @@#{sym} = nil
9
+ end
10
+
11
+ def self.#{sym}
12
+ @@#{sym}
13
+ end
14
+
15
+ def #{sym}
16
+ @@#{sym}
17
+ end
18
+ EOS
19
+ end
20
+ end
21
+
22
+ def mattr_writer(*syms)
23
+ syms.each do |sym|
24
+ class_eval(<<-EOS, __FILE__, __LINE__)
25
+ unless defined? @@#{sym}
26
+ @@#{sym} = nil
27
+ end
28
+
29
+ def self.#{sym}=(obj)
30
+ @@#{sym} = obj
31
+ end
32
+
33
+ def #{sym}=(obj)
34
+ @@#{sym} = obj
35
+ end
36
+ EOS
37
+ end
38
+ end
39
+
40
+ def mattr_accessor(*syms)
41
+ mattr_reader(*syms)
42
+ mattr_writer(*syms)
43
+ end
44
+ end
data/lib/my_wrapper.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+ require_gem 'facets', '>=1.8.51'
3
+ require 'facets/more/command'
4
+ require 'pp'
5
+ require 'stringio'
6
+
7
+ class MyWrapperCommand < Console::Command
8
+
9
+ def initialize(*args)
10
+ @missing_options = []
11
+ super
12
+ end
13
+
14
+ #-----------------------------------------------------------------------------------------------------------------------------
15
+ # Default/dynamic behavior
16
+
17
+ # Any subcommands that we haven't implemented here will simply be passed on to the built-in svn command.
18
+ # def method_missing(subcommand, *args)
19
+ # puts "in method_missing(#{subcommand}, #{args.inspect})"
20
+ # p options
21
+ # exec subcommand, *args
22
+ # end
23
+
24
+ def option_missing(option_name, args)
25
+ puts "in option_missing (#{option_name.inspect}, #{args.inspect})"
26
+
27
+ # The following hackery 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.
28
+ # 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 @missing_options, and later we will add them back on.
29
+ # 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*!
30
+ # But...it seems to work to just pretend they're options.
31
+ # 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.
32
+ # 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.
33
+ # Alternatives considered:
34
+ # * 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'")
35
+ # * 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*.
36
+ # Solution for wrappers:?
37
+ # pass_through :some_built_in_subcommand
38
+ # Tells Command to not parse options out of args -- just pass *all* args (options and all) on to the subcommand's method(*args).
39
+ # Ideally, we wouldn't be using option_missing at all because all options would be listed in the respective subcommand module...but we're too lazy to list them all out, so this is just seemed like the easiest way to do things....
40
+
41
+ @missing_options << "#{option_name}" << args
42
+ @missing_options.flatten!
43
+
44
+ return arity = args.size
45
+ end
46
+
47
+ #-----------------------------------------------------------------------------------------------------------------------------
48
+ module Diff
49
+ def __color
50
+ @diff_command = 'colordiff'
51
+ end
52
+ end
53
+ def diff(*args)
54
+ puts "in diff (#{args.inspect})"
55
+ args << '--diff-cmd' << @diff_command if @diff_command
56
+ exec 'diff', *args
57
+ end
58
+
59
+ #-----------------------------------------------------------------------------------------------------------------------------
60
+ # Helpers
61
+
62
+ private
63
+ def exec(*args)
64
+ args = ['svn'] + args + @missing_options
65
+ # options comes last because once the options parsing starts, it eats all args up to the very end
66
+ puts "This is the command we would execute at this point, if this weren't just a demonstration"
67
+ p args
68
+ end
69
+
70
+
71
+ end
72
+ MyWrapperCommand.execute
data/lib/subversion.rb ADDED
@@ -0,0 +1,335 @@
1
+ # Tested by: ../test/subversion_test.rb
2
+ $loaded ||= {}; if !$loaded[File.expand_path(__FILE__)]; $loaded[File.expand_path(__FILE__)] = true;
3
+
4
+ require 'fileutils'
5
+ require 'rexml/document'
6
+ require 'rexml/xpath'
7
+ require 'rubygems'
8
+
9
+ require_gem 'facets', '>=1.8.51'
10
+ require 'facets/core/kernel/require_local'
11
+ require 'facets/core/enumerable/uniq_by'
12
+ require 'facets/core/kernel/silence_stream'
13
+
14
+ require_gem 'our_extensions', '>=0.0.2'
15
+ require 'capture_output'
16
+
17
+ # Had a lot of trouble getting ActiveSupport to load without giving errors! Eventually gave up on that idea since I only needed it for mattr_accessor and Facets supplies that.
18
+ #require_gem 'activesupport' # mattr_accessor
19
+ #require 'active_support'
20
+ #require 'active_support/core_ext/module/attribute_accessors'
21
+ #require 'facets/core/class/cattr'
22
+ require_local "attribute_accessors"
23
+
24
+ # Wraps the Subversion shell commands for Ruby.
25
+ module Subversion
26
+ # True if you want output from svn to be colorized (useful if output is for human eyes, but not useful if using the output programatically)
27
+ @@color = false
28
+ mattr_accessor :color
29
+
30
+ # If true, will only output which command _would_ have been executed but will not actually execute it.
31
+ @@dry_run = false
32
+ mattr_accessor :dry_run
33
+
34
+ # If true, will print all commands to the screen before executing them.
35
+ @@print_commands = false
36
+ mattr_accessor :print_commands
37
+
38
+ # Adds the given items to the repository. Items may contain wildcards.
39
+ def self.add(*args)
40
+ execute "add #{args.join ' '}"
41
+ end
42
+
43
+ # Sets the svn:ignore property based on the given +patterns+.
44
+ # Each pattern is both the path (where the property gets set) and the property itself.
45
+ # For instance:
46
+ # "log/*.log" would add "*.log" to the svn:ignore property on the log/ directory.
47
+ # "log" would add "log" to the svn:ignore property on the ./ directory.
48
+ def self.ignore(*patterns)
49
+
50
+ patterns.each do |pattern|
51
+ path = File.dirname(pattern)
52
+ path += '/' if path == '.'
53
+ pattern = File.basename(pattern)
54
+ add_to_property 'ignore', path, pattern
55
+ end
56
+ nil
57
+ end
58
+ def self.unignore(*patterns)
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # Adds the given repo URL (http://svn.yourcompany.com/path/to/something) as an svn:externals.
63
+ #
64
+ # Options may include:
65
+ # * +:as+ - overrides the default behavior of naming the checkout based on the last component of the repo path
66
+ # * +:local_path+ - specifies where to set the externals property. Defaults to './'
67
+ #
68
+ def self.externalize(repo_url, options = {})
69
+ options[:local_path] ||= './'
70
+ options[:as] ||= File.basename(repo_url)
71
+ options[:as] = options[:as].ljust(29)
72
+ add_to_property 'externals', options[:local_path], "#{options[:as]} #{repo_url}"
73
+ end
74
+
75
+ # Removes the given items from the repository and the disk. Items may contain wildcards.
76
+ def self.remove(*args)
77
+ execute "rm #{args.join ' '}"
78
+ end
79
+
80
+ # Removes the given items from the repository and the disk. Items may contain wildcards.
81
+ # To do: add a :force => true option to remove
82
+ def self.remove_force(*args)
83
+ execute "rm --force #{args.join ' '}"
84
+ end
85
+
86
+ # Removes the given items from the repository BUT NOT THE DISK. Items may contain wildcards.
87
+ def self.remove_without_delete(*args)
88
+ # resolve the wildcards before iterating
89
+ args.collect {|path| Dir[path]}.flatten.each do |path|
90
+ entries_file = "#{File.dirname(path)}/.svn/entries"
91
+ File.chmod(0644, entries_file)
92
+
93
+ xmldoc = REXML::Document.new(IO.read(entries_file))
94
+ # first attempt to delete a matching entry with schedule == add
95
+ unless xmldoc.root.elements.delete "//entry[@name='#{File.basename(path)}'][@schedule='add']"
96
+ # then attempt to alter a missing schedule to schedule=delete
97
+ entry = REXML::XPath.first(xmldoc, "//entry[@name='#{File.basename(path)}']")
98
+ entry.attributes['schedule'] ||= 'delete' if entry
99
+ end
100
+ # write back to the file
101
+ File.open(entries_file, 'w') { |f| xmldoc.write f, 0 }
102
+
103
+ File.chmod(0444, entries_file)
104
+ end
105
+ end
106
+
107
+ # Reverts the given items in the working copy. Items may contain wildcards.
108
+ def self.revert(*args)
109
+ execute "revert #{args.join ' '}"
110
+ end
111
+
112
+ # Marks the given items as being executable. Items may _not_ contain wildcards.
113
+ def self.make_executable(*paths)
114
+ paths.each do |path|
115
+ self.set_property 'executable', '', path
116
+ end
117
+ end
118
+ def self.make_not_executable(*paths)
119
+ paths.each do |path|
120
+ self.delete_property 'executable', path
121
+ end
122
+ end
123
+
124
+ # Returns the status of items in the working directories +paths+. Returns the raw output from svn (use <tt>split("\n")</tt> if you want an array).
125
+ def self.status(*args)
126
+ args = ['./'] if args.empty?
127
+ execute("status #{args.join ' '}")
128
+ end
129
+
130
+ def self.status_against_server(*args)
131
+ args = ['./'] if args.empty?
132
+ self.status('-u', *args)
133
+ end
134
+
135
+ def self.update(*args)
136
+ args = ['./'] if args.empty?
137
+ execute("update #{args.join ' '}")
138
+ end
139
+
140
+ def self.status_the_section_before_externals(path = './')
141
+ status = status(path) || ''
142
+ status.sub!(/(Performing status.*)/m, '')
143
+ end
144
+
145
+ # Returns an array of external *items*
146
+ # Example:
147
+ # gemables/our_extensions/tasks/shared
148
+ # gemables/our_extensions/doc_include/template
149
+ def self.externals_items(path = './')
150
+ status = status_the_section_before_externals(path)
151
+ return [] if status.nil?
152
+ status.select { |line|
153
+ line =~ /^X/
154
+ }.map { |line|
155
+ # Just keep the filename part
156
+ line =~ /^X\s+(.+)/
157
+ $1
158
+ }
159
+ end
160
+
161
+ # Returns an array of ExternalsContainer objects representing all externals *containers* in the working directory specified by +path+.
162
+ def self.externals_containers(path = './')
163
+ # Using self.externals_items is kind of a cheap way to do this, and it results in some redundancy that we have to filter out
164
+ # (using uniq_by), but it seemed more efficient than the alternative (traversing the entire directory tree and querying for
165
+ # `svn prepget svn:externals` at each stop to see if the directory is an externals container).
166
+ self.externals_items(path).map { |external_dir|
167
+ ExternalsContainer.new(external_dir + '/..')
168
+ }.uniq_by { |external|
169
+ external.container_dir
170
+ }
171
+ end
172
+
173
+ # Returns the local modifications to the working directory specified by +path+.
174
+ def self.diff(*args)
175
+ args = ['./'] if args.empty?
176
+ #puts("diff #{"--diff-cmd colordiff" if color} #{args.join ' '}")
177
+ execute("diff #{"--diff-cmd colordiff" if color} #{args.join ' '}")
178
+ end
179
+
180
+ # It's easy to get/set properties, but less easy to add to a property. This method uses get/set to simulate add.
181
+ # It will uniquify lines, removing duplicates. (:todo: what if we want to set a property to have some duplicate lines?)
182
+ def self.add_to_property(property, path, *new_lines)
183
+ # :todo: I think it's possible to have properties other than svn:* ... so if property contains a prefix (something:), use it; else default to 'svn:'
184
+
185
+ # Get the current properties
186
+ lines = self.get_property(property, path).split "\n"
187
+ puts "Existing lines: #{lines.inspect}" if $debug
188
+
189
+ # Add the new lines, delete empty lines, and uniqueify all elements
190
+ lines.concat(new_lines).uniq!
191
+ puts "After concat(new_lines).uniq!: #{lines.inspect}" if $debug
192
+
193
+ lines.delete ''
194
+ # Set the property
195
+ puts "About to set propety to: #{lines.inspect}" if $debug
196
+ self.set_property property, lines.join("\n"), path
197
+ end
198
+
199
+ def self.get_property(property, path = './')
200
+ execute "propget svn:#{property} #{path}"
201
+ end
202
+ def self.delete_property(property, path = './')
203
+ execute "propdel svn:#{property} #{path}"
204
+ end
205
+ def self.set_property(property, value, path = './')
206
+ execute "propset svn:#{property} '#{value}' #{path}"
207
+ end
208
+
209
+ def self.make_directory(dir)
210
+ execute "mkdir #{dir}"
211
+ end
212
+
213
+ def self.help(*args)
214
+ execute "help #{args.join(' ')}"
215
+ end
216
+
217
+ def self.log(*args)
218
+ args = ['./'] if args.empty?
219
+ execute "log #{args.join(' ')}"
220
+ end
221
+ def self.latest_revision(*args)
222
+ args = ['./'] if args.empty?
223
+ matches = /Status against revision:\s+(\d+)/m.match(status_against_server(args))
224
+ matches && matches[1]
225
+ end
226
+
227
+ def self.info(*args)
228
+ args = ['./'] if args.empty?
229
+ execute "info #{args.join(' ')}"
230
+ end
231
+
232
+ # :todo: needs some serious unit-testing love
233
+ def self.base_url(path_or_url = './')
234
+ base_url = nil # needed so that base_url variable isn't local to if block!
235
+ started_using_dot_dots = false
236
+ loop do
237
+ matches = /URL: (.+)/.match(info(path_or_url))
238
+ if matches && matches[1]
239
+ base_url = matches[1]
240
+ else
241
+ break base_url
242
+ end
243
+
244
+ # Keep going up the path, one directory at a time, until `svn info` no longer returns a URL (will probably eventually return 'svn: PROPFIND request failed')
245
+ if path_or_url.include?('/') && !started_using_dot_dots
246
+ path_or_url = File.dirname(path_or_url)
247
+ else
248
+ started_using_dot_dots = true
249
+ path_or_url = File.join(path_or_url, '..')
250
+ end
251
+ #puts 'going up to ' + path_or_url
252
+ end
253
+ end
254
+
255
+ # The location of the executable to be used
256
+ def self.executable
257
+ @@executable ||=
258
+ ENV['PATH'].split(':').each do |dir|
259
+ # if File.exist?(executable = "#{dir}/svn")
260
+ # puts executable
261
+ # end
262
+ if File.exist?(executable = "#{dir}/svn") and #
263
+ `file #{executable}` !~ /ruby/ # We want to wrap the svn command provided by Subversion, not our custom replacement for that.
264
+ return executable
265
+ end
266
+ end
267
+ #
268
+ end
269
+
270
+ protected
271
+ def self.execute(*args)
272
+ options = args.last.is_a?(Hash) ? args.pop : {}
273
+ method = options.delete(:method) || :capture
274
+
275
+ command = "#{executable} #{args.join ' '}"
276
+ actually_execute(method, command)
277
+ end
278
+ # This abstraction exists to assist with unit tests. Test cases can simply override this function so that no external commands need to be executed.
279
+ def self.actually_execute(method, command)
280
+ if Subversion.dry_run && !$ignore_dry_run_option
281
+ puts "In execute(). Was about to execute this command via method :#{method}:"
282
+ p command
283
+ end
284
+ if Subversion.print_commands
285
+ p command
286
+ end
287
+
288
+ valid_options = [:capture, :exec, :popen]
289
+ case method
290
+ when :capture
291
+ silence_stream($stderr) {
292
+ `#{command}`
293
+ }
294
+ when :exec
295
+ #Kernel.exec *args
296
+ Kernel.exec command
297
+ when :popen
298
+ # To do: rather than `...`, which waits until command is finished before you get any output, maybe do popen and output the output in realtime!
299
+ IO.popen(command, 'r') do |pipe|
300
+ pipe.read
301
+ end
302
+ else
303
+ raise ArgumentError.new(":method option must be one of #{valid_options.inspect}")
304
+ end unless (Subversion.dry_run && !$ignore_dry_run_option)
305
+ end
306
+ end
307
+
308
+ module Subversion
309
+ # Represents an "externals container", which is a directory that has the <tt>svn:externals</tt> property set to something useful.
310
+ # Each ExternalsContainer contains a set of "entries", which are the actual directories listed in the <tt>svn:externals</tt>
311
+ # property and are "pulled into" the directory.
312
+ class ExternalsContainer
313
+ attr_reader :container_dir
314
+ attr_reader :entries
315
+
316
+ def initialize(external_dir)
317
+ @container_dir = File.expand_path(external_dir)
318
+ @entries = Subversion.get_property("externals", @container_dir)
319
+ #p @entries
320
+ end
321
+
322
+ def to_s
323
+ "#{container_dir}\n" +
324
+ entries.chomp.map { |line|
325
+ " * " + line
326
+ }.join
327
+ end
328
+
329
+ def ==(other)
330
+ self.container_dir == other.container_dir
331
+ end
332
+ end
333
+ end
334
+
335
+ end