sub 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,13 @@
1
+ == 0.4.0
2
+
3
+ * "sub ext" command to deal with svn:externals property (see "sub help")
4
+ * "sub ignore" command to either print or add to a directory's svn:ignore
5
+ property
6
+ * If you replace an external with a "real" directory, then use "sub up" in
7
+ another workspace, it will now do the right thing and remove the external
8
+ before updating the directory. (This solves what is, IMHO, a bug in svn --
9
+ it gets confused and attempts to grab its own lock and aborts.)
10
+
1
11
  == 0.3.0 2007-10-02
2
12
 
3
13
  * Bugfix: checks out externals that were added since last update
@@ -10,5 +10,6 @@ lib/sub/array.rb
10
10
  lib/sub/external.rb
11
11
  lib/sub/root.rb
12
12
  lib/sub/status.rb
13
+ lib/sub/sys.rb
13
14
  lib/sub/version.rb
14
15
  tasks/environment.rake
data/lib/sub.rb CHANGED
@@ -1,5 +1,6 @@
1
- require 'sub/app'
2
1
  require 'sub/array'
2
+ require 'sub/sys'
3
+ require 'sub/app'
3
4
  require 'sub/external'
4
5
  require 'sub/status'
5
6
  require 'sub/version'
@@ -1,23 +1,29 @@
1
1
  #!/usr/local/bin/ruby
2
2
  require 'benchmark'
3
3
  require 'fileutils'
4
-
5
- QUIET = 0
6
- NORMAL = 1
7
- VERBOSE = 2
4
+ require 'sub/sys'
8
5
 
9
6
  class Sub
7
+ include Sys::Client
8
+
10
9
  DEFAULT_BASE_URL = "svn+ssh://rubyforge.org/var/svn"
11
10
 
12
11
  def self.from_args(args)
13
- verbosity = NORMAL
14
12
  still_parsing_options = true
15
- command = :up
13
+ command = defaults[:command]
16
14
  options = {}
17
- while still_parsing_options
15
+ real_args = []
16
+ while !args.empty?
18
17
  case args[0]
19
- when 'up', 'co', 'help'
18
+
19
+ # commands
20
+ when 'up', 'co', 'help', 'ext', 'ignore'
20
21
  options[:command] = args[0]
22
+
23
+ # no-param options
24
+ when '--version'
25
+ puts "Sub version #{Sub::VERSION::STRING}"
26
+ exit 0
21
27
  when '-v', '--verbose'
22
28
  options[:verbosity] = VERBOSE
23
29
  when '-q', '--quiet'
@@ -27,34 +33,41 @@ class Sub
27
33
  still_parsing_options = false
28
34
  when '-c', '--clean'
29
35
  options[:clean] = true
36
+
37
+ # one-param options
30
38
  when '--url'
31
39
  args.shift
32
40
  options[:url] = args[0]
33
- when '--version'
34
- puts "Sub version #{Sub::VERSION::STRING}"
35
- exit 0
41
+ when '--revision', '-r'
42
+ args.shift
43
+ options[:revision] = args[0]
44
+ when '--directory', '--dir', '-d'
45
+ args.shift
46
+ options[:dir] = args[0]
47
+
36
48
  else
37
- still_parsing_options = false
49
+ real_args << args[0]
38
50
  end
39
- args.shift if still_parsing_options
51
+ args.shift
40
52
  end
41
53
 
42
- Sub.new(options, args)
54
+ Sub.new(options, real_args)
43
55
  end
44
56
 
45
- attr_reader :clean, :command, :args, :url
46
- attr_accessor :verbosity
57
+ attr_reader :clean, :command, :args, :url, :revision, :dir
47
58
 
48
59
  def initialize(options = {:verbosity => NORMAL}, args = [])
49
- options = defaults.merge(options)
50
- @verbosity = options[:verbosity]
60
+ options = Sub.defaults.merge(options)
61
+ sys.verbosity = options[:verbosity]
51
62
  @clean = options[:clean]
52
63
  @command = options[:command]
53
- @url = options[:url] || ENV['SUB_BASE_URL'] || ENV['SVN'] || DEFAULT_BASE_URL
64
+ @url = options[:url]
65
+ @revision = options[:revision]
66
+ @dir = options[:dir] || '.'
54
67
  @args = args
55
68
  end
56
69
 
57
- def defaults
70
+ def self.defaults
58
71
  {
59
72
  :verbosity => NORMAL,
60
73
  :command => :up,
@@ -62,7 +75,7 @@ class Sub
62
75
  end
63
76
 
64
77
  def execute
65
- self.send(@command)
78
+ puts self.send(@command)
66
79
  end
67
80
 
68
81
  # commands
@@ -71,6 +84,7 @@ class Sub
71
84
  @args = [`pwd`.chomp]
72
85
  end
73
86
  update_many(@args)
87
+ "Update complete."
74
88
  end
75
89
 
76
90
  def co
@@ -80,7 +94,37 @@ class Sub
80
94
 
81
95
  project = args.shift
82
96
  dir_name = args.shift || project
83
- svn("co #{url}/#{project}/trunk #{dir_name}")
97
+ svn("co #{base}/#{project}/trunk #{dir_name}")
98
+ "Checkout complete."
99
+ end
100
+
101
+ def base
102
+ url || ENV['SUB_BASE_URL'] || ENV['SVN'] || DEFAULT_BASE_URL
103
+ end
104
+
105
+ #todo: test
106
+ def ext
107
+ external_name = args.shift
108
+ if external_name.nil?
109
+ External.externals_of(dir).join("\n")
110
+ else
111
+ old_external = get_external(dir, external_name)
112
+ if url || revision
113
+ revision_to_set = revision == 'HEAD' ? nil : revision
114
+ set_external(dir, external_name, url || old_external.url, revision_to_set)
115
+ else
116
+ old_external
117
+ end
118
+ end
119
+ end
120
+
121
+ def ignore
122
+ if args.empty?
123
+ svn("propget svn:ignore #{dir}", true).strip
124
+ else
125
+ svn("propset svn:ignore '#{args.join("\n")}' #{dir}")
126
+ "#{dir} now ignoring #{args.join(", ")}"
127
+ end
84
128
  end
85
129
 
86
130
  def help
@@ -90,13 +134,31 @@ sub - Alex's wrapper for subversion
90
134
  Version: #{Sub::VERSION::STRING}
91
135
 
92
136
  Usage:
93
- sub co project_name [dir_name]
94
- checks out [base_url]/project_name/trunk into ./project_name (or dir_name if specified)
95
- sub up [dir]*
137
+
138
+ sub co [--url base_url] project [dir]
139
+ checks out [base_url]/project/trunk into ./project (or dir if specified)
140
+ default base_url is ENV['SUB_BASE_URL'] or ENV['SVN']
141
+ or #{DEFAULT_BASE_URL}
142
+
143
+ sub up [--clean] [dir]*
96
144
  fast update (default command, so 'sub dir...' or just 'sub' work too)
145
+ if --clean is specified, removes all unversioned files and directories
146
+
97
147
  sub help
98
148
  prints this message
99
149
 
150
+ sub ext [--dir directory] [external_name [--revision rev] [--url url]]]
151
+ prints the value of the named svn:external property for directory dir
152
+ if no dir is specified, uses '.'
153
+ if no external_name is specified, prints all externals in that directory
154
+ if --url or --revision is specified, sets the value and commits the change
155
+ if rev is HEAD then the revision is removed from that external
156
+
157
+ sub ignore [--dir directory] [pattern]
158
+ prints the value of the svn:ignore property for directory dir
159
+ if pattern is specified, ignores that pattern
160
+ if no dir is specified, uses '.'
161
+
100
162
  Options:
101
163
  --verbose, -v
102
164
  lots of output
@@ -104,15 +166,11 @@ Options:
104
166
  no output at all except for errors
105
167
  --help, -h
106
168
  prints this message
107
- --clean, -c
108
- 'up' command removes all unversioned files and directories
109
- --url [base_url]
110
- sets base repository url for 'co' command
111
- (default is ENV['SUB_BASE_URL'] or ENV['SVN'] or #{DEFAULT_BASE_URL})
112
169
  --version
113
170
  prints release version
114
171
  """
115
172
  end
173
+
116
174
 
117
175
  # methods
118
176
 
@@ -125,47 +183,38 @@ Options:
125
183
  end
126
184
 
127
185
  def update(root)
128
- Root.new(root, self, clean).update
186
+ Root.new(root, clean).update
129
187
  end
130
188
 
131
- def externals(root)
132
- Root.new(root, self, clean).externals
189
+ def all_externals(root)
190
+ Root.new(root, clean).all_externals
191
+ end
192
+
193
+ def get_external(dir, external_name)
194
+ External.externals_of(dir).select do |external|
195
+ external.name == external_name
196
+ end.first
133
197
  end
134
198
 
135
- def parse_externals(st)
136
- exts = []
137
- st.split("\n").select do |line|
138
- line =~ /^X/
139
- end.collect do |line|
140
- line.gsub(/^X */, '').gsub(/\/[^\/]*$/, '')
141
- end.uniq.collect do |parent|
142
- prop = `svn propget svn:externals #{parent}`
143
- prop.split("\n").each do |external|
144
- next if external.strip.empty?
145
- exts << External.new(parent, external, self)
146
- end
199
+ def set_external(dir, external_name, url, revision = nil)
200
+ if get_external(dir, external_name)
201
+ remove_external(dir, external_name)
147
202
  end
148
- exts
203
+ new_external = External.new(dir, external_name, url, revision)
204
+ externals = External.externals_of(dir) << new_external
205
+ set_externals(dir, externals)
206
+ new_external
149
207
  end
150
-
151
- def say(msg)
152
- puts msg if verbosity > QUIET
208
+
209
+ def set_externals(externals_path, externals)
210
+ property_lines = externals.collect { |x| x.to_s }
211
+ svn "propset svn:externals '#{property_lines.join("\n")}' #{externals_path}"
153
212
  end
154
-
155
- def svn(cmd)
156
- svncmd = "svn"
157
- svncmd += " --quiet" if (cmd =~ /^(up|co)/ && verbosity == QUIET)
158
- run("#{svncmd} #{cmd}")
213
+
214
+ def remove_external(dir, name)
215
+ set_externals(dir, External.externals_of(dir).select { |x| x.name != name })
216
+ svn("ci --non-recursive -m 'removing external #{name}' #{dir}")
217
+ FileUtils.rm_rf("#{dir}/#{name}") # TODO: unless modified
159
218
  end
160
219
 
161
- def run(cmd, return_output = false)
162
- say("\t#{cmd}") if verbosity == VERBOSE
163
- if (return_output)
164
- `#{cmd}`
165
- else
166
- cmd += ">/dev/null"if verbosity == QUIET
167
- system(cmd)
168
- end
169
- end
170
220
  end
171
-
@@ -1,36 +1,31 @@
1
1
  class External
2
+ include Sys::Client
2
3
 
3
- def self.externals_in_directory(parent, app)
4
+ def self.externals_of(parent, prop = `svn propget svn:externals #{parent}`)
4
5
  exts = []
5
- prop = `svn propget svn:externals #{parent}`
6
6
  prop.split("\n").each do |prop_line|
7
7
  next if prop_line.strip.empty?
8
- exts << External.new(parent, prop_line, app)
8
+ exts << External.from_property_line(parent, prop_line)
9
9
  end
10
10
  exts
11
11
  end
12
12
 
13
- attr_reader :name, :path, :url, :revision, :app
13
+ attr_reader :name, :path, :url, :revision
14
14
 
15
- def initialize(parent, property, app = nil)
16
- match = /^(\S+)\s+(-r\s*\d*\s+)?(\S+)\s*$/.match(property)
17
- @name = match[1]
18
- @revision = match[2] ? match[2].gsub(/\D/,'').to_i : nil
19
- @url = match[3]
20
- @path = "#{parent}/#{@name}"
21
- @app = app
22
- end
23
-
24
- def say(msg)
25
- app.say(msg)
15
+ def self.from_property_line(parent, property_line)
16
+ match = /^(\S+)\s+(-r\s*\d*\s+)?(\S+)\s*$/.match(property_line)
17
+ name = match[1]
18
+ revision = match[2] ? match[2].gsub(/\D/,'').to_i : nil
19
+ url = match[3]
20
+ raise "no url found for '#{property_line}'" if url.nil?
21
+ new(parent, name, url, revision)
26
22
  end
27
-
28
- def svn(cmd)
29
- app.svn(cmd)
30
- end
31
-
32
- def run(cmd, return_output = false)
33
- app.run(cmd, return_output)
23
+
24
+ def initialize(parent, name, url, revision = nil)
25
+ @name = name
26
+ @revision = revision
27
+ @url = url
28
+ @path = "#{parent}/#{@name}"
34
29
  end
35
30
 
36
31
  def ==(other)
@@ -38,7 +33,9 @@ class External
38
33
  end
39
34
 
40
35
  def to_s
41
- "External[name=#{name}, path=#{path}, url=#{url}, revision=#{revision}]"
36
+ "#{name} " +
37
+ (revision ? "-r #{revision} " : "") +
38
+ url
42
39
  end
43
40
 
44
41
  def revision_actual
@@ -52,10 +49,11 @@ class External
52
49
 
53
50
  #todo: test? (indirectly tested via root.rb)
54
51
  def update
52
+ raise "path is nil" if path.nil?
55
53
  say "Updating external #{path}"
56
54
  run("svn cleanup #{path}")
57
55
  rev = revision.nil? ? '' : "-r#{revision}"
58
- svn("up #{rev} #{path} | grep -v 'At revision'")
56
+ svn("up #{rev} #{path}", true)
59
57
  end
60
58
 
61
59
  def checkout
@@ -1,25 +1,14 @@
1
1
  class Root
2
- attr_reader :app, :root_path
2
+ include Sys::Client
3
+
4
+ attr_reader :root_path
3
5
 
4
- def initialize(root_path, app, clean = false)
6
+ def initialize(root_path, clean = false)
5
7
  @root_path = root_path
6
- @app = app
7
8
  @clean = clean
8
9
  @status = nil
9
10
  end
10
11
 
11
- def say(msg)
12
- app.say(msg)
13
- end
14
-
15
- def svn(cmd)
16
- app.svn(cmd)
17
- end
18
-
19
- def run(cmd, return_output = false)
20
- app.run(cmd, return_output)
21
- end
22
-
23
12
  class Processes < Array
24
13
  def initialize
25
14
  end
@@ -43,12 +32,14 @@ class Root
43
32
  remove_unversioned
44
33
  end
45
34
 
46
- externals_before = externals
35
+ remove_deleted_externals
36
+
37
+ externals_before = all_externals
47
38
 
48
39
  svn("up --ignore-externals #{root_path}")
49
40
  # for some reason (array - array) doesn't work right here
50
41
  # so i had to write my own subtract method
51
- removed_externals = externals_before.subtract(externals)
42
+ removed_externals = externals_before.subtract(all_externals)
52
43
  removed_externals.each do |ext|
53
44
  say "Removed external #{ext}"
54
45
  FileUtils.rm_rf(ext.path)
@@ -57,7 +48,7 @@ class Root
57
48
  @processes = Processes.new
58
49
 
59
50
  already_up_to_date = []
60
- externals.each do |external|
51
+ all_externals.each do |external|
61
52
  if external.should_update?
62
53
  if File.exists?(external.path)
63
54
  update_external(external)
@@ -90,6 +81,37 @@ class Root
90
81
  end
91
82
  end
92
83
 
84
+ def remove_deleted_externals
85
+ directories_containing_externals.each do |dir|
86
+ deleted_something = false
87
+ workspace_externals = External.externals_of(dir)
88
+ Dir.chdir(root_path) do
89
+ head_property = svn("propget -r HEAD svn:externals #{dir}", true)
90
+ head_externals = External.externals_of(dir, head_property)
91
+ deleted_externals = workspace_externals.select do |workspace_external|
92
+ head_externals.select { |head_external| head_external.name == workspace_external.name }.empty?
93
+ end
94
+ unless deleted_externals.empty?
95
+ deleted_externals.each do |external|
96
+ # todo: check for local changes
97
+ say("Deleting external #{external.path}")
98
+ FileUtils.rm_rf(external.path)
99
+ end
100
+ # update the property first
101
+ svn("up -N #{dir}")
102
+ # then update all the deleted former externals
103
+ svn("up " + deleted_externals.map{|external| "#{external.path}"}.join(" "))
104
+ end
105
+ end
106
+ if deleted_something
107
+ svn("up -N #{dir}")
108
+ svn("up --ignore-externals #{dir}")
109
+ puts "pausing"
110
+ sleep(9999)
111
+ end
112
+ end
113
+ end
114
+
93
115
  def remove_unversioned
94
116
  status.unversioned.each do |path|
95
117
  if File.directory?(path)
@@ -102,13 +124,13 @@ class Root
102
124
  end
103
125
 
104
126
  def status
105
- @status ||= Status.new(run("svn st #{root_path}", true))
127
+ @status ||= Status.new(svn("st #{root_path}", true))
106
128
  end
107
129
 
108
- def externals
130
+ def all_externals
109
131
  exts = []
110
132
  directories_containing_externals.collect do |parent|
111
- exts += External.externals_in_directory(parent, app)
133
+ exts += External.externals_of(parent)
112
134
  end
113
135
  exts
114
136
  end
@@ -116,7 +138,7 @@ class Root
116
138
  def directories_containing_externals
117
139
  status.externals.collect do |path|
118
140
  if (path !~ /\//)
119
- "."
141
+ root_path
120
142
  else
121
143
  path.gsub(/\/[^\/]*$/, '')
122
144
  end
@@ -25,7 +25,6 @@ class Status
25
25
  end
26
26
  end
27
27
 
28
-
29
28
  def initialize(text)
30
29
  @lines = text.split("\n").collect do |line|
31
30
  Status::Line.new(line)
@@ -0,0 +1,73 @@
1
+ QUIET = 0
2
+ NORMAL = 1
3
+ VERBOSE = 2
4
+
5
+ class Sys
6
+
7
+ module Client
8
+ def sys
9
+ Sys.instance
10
+ end
11
+
12
+ #todo: look up ruby dynamic proxy
13
+ def say(msg)
14
+ sys.say(msg)
15
+ end
16
+
17
+ def svn(cmd, return_output = false)
18
+ sys.svn(cmd, return_output)
19
+ end
20
+
21
+ def run(cmd, return_output = false)
22
+ sys.run(cmd, return_output)
23
+ end
24
+
25
+ def verbosity=(level)
26
+ sys.verbosity = level
27
+ end
28
+
29
+ def verbosity
30
+ sys.verbosity
31
+ end
32
+ end
33
+
34
+ @@instance = Sys.new
35
+
36
+ def self.instance
37
+ @@instance
38
+ end
39
+
40
+ attr_accessor :verbosity
41
+
42
+ def initialize(verbosity = NORMAL)
43
+ @verbosity = verbosity
44
+ end
45
+
46
+ def say(msg)
47
+ puts msg if verbosity > QUIET
48
+ end
49
+
50
+ def svn(cmd, return_output = false)
51
+ svncmd = "svn"
52
+ svncmd += " --quiet" if (cmd =~ /^(up|co)/ && verbosity == QUIET)
53
+ output = run("#{svncmd} #{cmd}", return_output)
54
+ puts output if output && verbosity == VERBOSE
55
+ output
56
+ end
57
+
58
+ def run(cmd, return_output = false)
59
+ val = nil
60
+ say("\t#{cmd}") if verbosity == VERBOSE
61
+ if (return_output)
62
+ val = `#{cmd}`
63
+ else
64
+ cmd += ">/dev/null"if verbosity == QUIET
65
+ system(cmd)
66
+ end
67
+ unless $?.success?
68
+ puts caller.join("\n\t")
69
+ raise "Failure: '#{cmd}' returned #{$?.exitstatus}"
70
+ end
71
+ val
72
+ end
73
+ end
@@ -1,7 +1,7 @@
1
1
  class Sub #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 3
4
+ MINOR = 4
5
5
  TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.2
2
+ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: sub
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.0
7
- date: 2007-10-02 00:00:00 -07:00
6
+ version: 0.4.0
7
+ date: 2007-11-18 00:00:00 -08:00
8
8
  summary: sub is a wrapper for svn that adds faster/safer updates, easy trunk checkout, etc.
9
9
  require_paths:
10
10
  - lib
@@ -41,6 +41,7 @@ files:
41
41
  - lib/sub/external.rb
42
42
  - lib/sub/root.rb
43
43
  - lib/sub/status.rb
44
+ - lib/sub/sys.rb
44
45
  - lib/sub/version.rb
45
46
  - tasks/environment.rake
46
47
  test_files: []