svn-command 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Readme +251 -0
- data/bin/command_completion_for_svn_command +20 -0
- data/bin/rscm_test +19 -0
- data/bin/svn +7 -0
- data/lib/attribute_accessors.rb +44 -0
- data/lib/my_wrapper.rb +72 -0
- data/lib/subversion.rb +335 -0
- data/lib/subversion_extensions.rb +60 -0
- data/lib/svn_command.rb +627 -0
- data/test/shared/test_helper.rb +3 -0
- data/test/shared/test_helpers/assertions.rb +56 -0
- data/test/shared/test_helpers/test_colorizer.rb +106 -0
- data/test/subversion_extensions_test.rb +66 -0
- data/test/subversion_test.rb +99 -0
- data/test/svn_command_test.rb +455 -0
- data/test/test_helper.rb +31 -0
- metadata +75 -0
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,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
|