svnauto 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. data/bin/sc +2 -10
  2. data/{lib/sc/constants.rb → bin/sva} +8 -13
  3. data/lib/{sc.rb → svnauto.rb} +13 -9
  4. data/lib/{sc → svnauto}/command.rb +8 -3
  5. data/lib/{sc → svnauto}/commands/bug.rb +5 -2
  6. data/lib/{sc → svnauto}/commands/checkout.rb +3 -1
  7. data/lib/{sc → svnauto}/commands/config.rb +18 -2
  8. data/lib/{sc → svnauto}/commands/create.rb +2 -1
  9. data/lib/{sc → svnauto}/commands/experimental.rb +19 -5
  10. data/lib/svnauto/commands/externals.rb +128 -0
  11. data/lib/{sc → svnauto}/commands/info.rb +1 -1
  12. data/lib/{sc → svnauto}/commands/list.rb +3 -1
  13. data/lib/{sc → svnauto}/commands/release.rb +39 -7
  14. data/lib/{sc → svnauto}/config_file.rb +3 -3
  15. data/lib/{sc/path.rb → svnauto/constants.rb} +24 -18
  16. data/lib/{sc → svnauto}/dispatcher.rb +11 -4
  17. data/lib/svnauto/path.rb +138 -0
  18. data/lib/{sc → svnauto}/project.rb +16 -11
  19. data/lib/{sc → svnauto}/repository.rb +2 -2
  20. data/lib/{sc → svnauto}/svn.rb +51 -10
  21. data/lib/svnauto/svn_externals.rb +187 -0
  22. data/lib/svnauto/svn_info.rb +96 -0
  23. data/lib/{sc → svnauto}/version.rb +2 -2
  24. data/test/setup.rb +24 -28
  25. data/test/test_bug.rb +10 -10
  26. data/test/test_checkout.rb +3 -3
  27. data/test/test_create.rb +3 -3
  28. data/test/test_experimental.rb +33 -18
  29. data/test/test_externals.rb +55 -0
  30. data/test/test_path.rb +148 -0
  31. data/test/test_release.rb +11 -11
  32. data/test/test_svninfo.rb +17 -0
  33. data/test/test_version.rb +16 -16
  34. metadata +35 -42
  35. data/INSTALL +0 -48
  36. data/LICENSE +0 -22
  37. data/README +0 -81
  38. data/THANKS +0 -8
  39. data/TODO +0 -8
  40. data/doc/manual.txt +0 -241
@@ -24,12 +24,12 @@
24
24
  ################################################################################
25
25
  require 'yaml'
26
26
  ################################################################################
27
- module SC
27
+ module SvnAuto
28
28
  ################################################################################
29
29
  class ConfigFile
30
30
  ################################################################################
31
- ENV_OVERRIDE = 'SC_CONFIG_FILE'
32
- FILENAME = File.join(ENV['HOME'], '.sc')
31
+ ENV_OVERRIDE = 'SvnAuto_CONFIG_FILE'
32
+ FILENAME = File.join(ENV['HOME'], '.sva')
33
33
 
34
34
  ################################################################################
35
35
  # load in the user's configuration file
@@ -22,30 +22,36 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  #
24
24
  ################################################################################
25
- module SC
26
- ################################################################################
27
- module Path
25
+ module SvnAuto
26
+ module Constants
28
27
  ################################################################################
29
- # convert a relative path to home to absolute
30
- def self.absolute_from_home (path)
31
- home = File.expand_path('~')
32
- path.strip!
28
+ # The version number for this copy of SvnAuto
29
+ VERSION = '1.1.0'
33
30
 
34
- # path may start with ~
35
- path = File.expand_path(path) if path[0, 1] == '~'
31
+ ################################################################################
32
+ # What to call myself
33
+ ME = 'sva'
36
34
 
37
- # allow path to be relative or absolute
38
- path[0, 1] == '/' ? path : File.join(home, path)
39
- end
35
+ ################################################################################
36
+ # This regex is used to replace special characters in project/branch
37
+ # names so that they don't have problems with the file system or shell
38
+ NAME_RE = /[^\w.,_+:-]/
40
39
 
41
40
  ################################################################################
42
- # make an absolute path relative to home
43
- def self.relative_to_home (path)
44
- home = File.expand_path('~')
45
- path[0, home.length] == home ? path.sub(home, '~') : path
46
- end
41
+ TERMINAL = HighLine.new
47
42
 
43
+ ################################################################################
44
+ # We use the HOME environment variable all the time, make sure it's set up
45
+ if ENV['HOME'].nil?
46
+ if !ENV['HOMEDRIVE'].nil? and !ENV['HOMEPATH'].nil?
47
+ ENV['HOME'] = ENV['HOMEDRIVE'] + ENV['HOMEPATH']
48
+ elsif !ENV['USERPROFILE'].nil?
49
+ ENV['HOME'] = ENV['USERPROFILE']
50
+ else
51
+ raise "please set your HOME environment variable"
52
+ end
53
+ end
54
+
48
55
  end
49
- ################################################################################
50
56
  end
51
57
  ################################################################################
@@ -25,7 +25,7 @@
25
25
  require 'abbrev'
26
26
  require 'optparse'
27
27
  ################################################################################
28
- module SC
28
+ module SvnAuto
29
29
  ################################################################################
30
30
  class Dispatcher
31
31
  ################################################################################
@@ -124,7 +124,7 @@ module SC
124
124
  # make sure the selected repository is in the config file
125
125
  if !klass.without_repository and !config.find_repository(@project.repository.name)
126
126
  error = "the selected repository (#{@project.repository.url}) is not in the configuration file, "
127
- error << "please use the 'sc config --add' command to add it first."
127
+ error << "please use the 'sva config --add' command to add it first."
128
128
  raise error
129
129
  end
130
130
 
@@ -163,10 +163,10 @@ module SC
163
163
 
164
164
  if klass.args_min and command_extras.length < klass.args_min
165
165
  puts usage_for(klass)
166
- exit 1
166
+ raise "you did not give enough arguments"
167
167
  elsif klass.args_max and command_extras.length > klass.args_max
168
168
  puts usage_for(klass)
169
- exit 1
169
+ raise "you gave too many arguments"
170
170
  end
171
171
 
172
172
  klass.new.run(project, command_extras)
@@ -184,6 +184,13 @@ module SC
184
184
  lines.first << "\nShortcuts: #{klass.name[1..-1].join(', ')}"
185
185
  end
186
186
 
187
+ if klass.example
188
+ lines.first << "\n\n\nExamples:\n"
189
+ klass.example.each do |example|
190
+ lines.first << " #{Constants::ME} #{name} #{example}\n"
191
+ end
192
+ end
193
+
187
194
  lines.first << "\n\nOptions for the #{name} command:"
188
195
  lines.join("\n")
189
196
  end
@@ -0,0 +1,138 @@
1
+ ################################################################################
2
+ #
3
+ # Copyright (C) 2006 Peter J Jones (pjones@pmade.com)
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #
24
+ ################################################################################
25
+ module SvnAuto
26
+ ################################################################################
27
+ module Path
28
+ ################################################################################
29
+ # make a relative path to home absolute
30
+ def self.absolute_from_home (path)
31
+ home = File.expand_path('~')
32
+ path.strip!
33
+
34
+ # path may start with ~
35
+ path = File.expand_path(path) if path[0, 1] == '~'
36
+
37
+ # allow path to be relative or absolute
38
+ absolute?(path) ? path : File.join(home, path)
39
+ end
40
+
41
+ ################################################################################
42
+ # make an absolute path relative to home
43
+ def self.relative_to_home (path)
44
+ home = File.expand_path('~')
45
+ absolute_path = path.dup
46
+
47
+ if case_insensitive_filesystem?
48
+ home.downcase!
49
+ absolute_path.downcase!
50
+ end
51
+
52
+ absolute_path[0, home.length] == home ? absolute_path.sub(home, '~') : absolute_path
53
+ end
54
+
55
+ ################################################################################
56
+ # make file:// url from local path.
57
+ def self.to_url (path)
58
+ if windows?
59
+ if unc?(path)
60
+ return URI.escape("file:#{path}")
61
+ elsif has_drive?(path)
62
+ return URI.escape("file:///#{path}")
63
+ end
64
+ end
65
+
66
+ url?(path) ? path : URI.escape("file://#{path}")
67
+ end
68
+
69
+ ################################################################################
70
+ # extract local path element from url.
71
+ def self.to_path (url)
72
+ unless windows?
73
+ return url.is_a?(URI) ? url.path : url
74
+ end
75
+
76
+ path = url.to_s
77
+ return path unless url?(path)
78
+ return path unless /^file:/ =~ path
79
+
80
+ path = path.sub(/^file:/, "")
81
+ return path unless path =~ /^\//
82
+
83
+ if has_drive?(path.sub(/^\/+/, ""))
84
+ path.sub(/^\/+/, "")
85
+ elsif unc?(path)
86
+ path
87
+ else
88
+ path.sub(/^\/+/, "/")
89
+ end
90
+ end
91
+
92
+ ################################################################################
93
+ # path is URL?
94
+ def self.url? (path)
95
+ begin
96
+ u = URI.parse(path)
97
+ return (u.scheme and u.scheme.length > 0)
98
+ rescue URI::InvalidURIError
99
+ false
100
+ end
101
+ end
102
+
103
+ ################################################################################
104
+ # path is absolute?
105
+ def self.absolute? (path)
106
+ Pathname.new(path).absolute?
107
+ end
108
+
109
+ ################################################################################
110
+ # Is this platform running the Windows OS?
111
+ def self.windows?
112
+ RUBY_PLATFORM.match(/djgpp|(cyg|ms|bcc)win|mingw/)
113
+ end
114
+
115
+ ################################################################################
116
+ # path has Windows drive letter?
117
+ def self.has_drive? (path)
118
+ path.match(/^[a-z]:/i)
119
+ end
120
+
121
+ ################################################################################
122
+ # path is UNC?
123
+ def self.unc? (path)
124
+ path.match(%r{^//[^/]})
125
+ end
126
+
127
+ ################################################################################
128
+ # are we running under a case insensitive filesystem? (such as Windows)
129
+ def self.case_insensitive_filesystem?
130
+ ruby_version = Version.new(RUBY_VERSION) rescue nil
131
+ return !File::FNM_SYSCASE.zero? if ruby_version >= Version.new('1.8.5')
132
+ return windows?
133
+ end
134
+
135
+ end
136
+ ################################################################################
137
+ end
138
+ ################################################################################
@@ -22,7 +22,7 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  #
24
24
  ################################################################################
25
- module SC
25
+ module SvnAuto
26
26
  ################################################################################
27
27
  class Project
28
28
  ################################################################################
@@ -40,12 +40,12 @@ module SC
40
40
  project = nil
41
41
 
42
42
  # see if we're in an svn directory
43
- Svn.info do |line|
44
- m = line.match(/^Repository\s+Root:\s+(.+)$/) and url = m[1]
45
- m = line.match(/^URL:\s+(.+)$/) and project = m[1]
46
- end
43
+ info = SvnInfo.for('.', false)
44
+
45
+ if info.status
46
+ url = info.repository_root
47
+ project = info.url
47
48
 
48
- if url and project
49
49
  # try to find a matching repository
50
50
  repository = config[:repositories].find {|r| r.url == url}
51
51
  repository ||= Repository.new(:url => url)
@@ -62,7 +62,7 @@ module SC
62
62
  ################################################################################
63
63
  # remove special characters from a project name
64
64
  def self.clean_name (non_clean_name)
65
- non_clean_name.gsub(/[^\w\d_-]+/, '_')
65
+ non_clean_name.gsub(Constants::NAME_RE, '_')
66
66
  end
67
67
 
68
68
  ################################################################################
@@ -120,7 +120,7 @@ module SC
120
120
  end
121
121
 
122
122
  ################################################################################
123
- # get a list of the directories that sc uses
123
+ # get a list of the directories that SvnAuto uses
124
124
  def directories
125
125
  [
126
126
  url,
@@ -141,7 +141,7 @@ module SC
141
141
  def create
142
142
  directories.each do |dir|
143
143
  unless Svn.has_path(dir)
144
- Svn.mkdir('-m', "'#{Constants::ME}: creating #{dir} for project #{name}.'", dir)
144
+ Svn.mkdir('-m', "#{Constants::ME}: creating #{dir} for project #{name}.", dir)
145
145
  end
146
146
  end
147
147
  end
@@ -232,11 +232,16 @@ module SC
232
232
  raise message
233
233
  end
234
234
 
235
- Svn.commit('-m', "'#{Constants::ME}: merging branch using tags #{start_tag} and #{end_tag}'")
235
+ Svn.commit('-m', "#{Constants::ME}: merging branch using tags #{start_tag} and #{end_tag}")
236
236
  end
237
237
 
238
238
  # clean up after merge
239
- system('rm', '-rf', dir) or raise "failed to remove tmp merge directory: #{dir}"
239
+ begin
240
+ FileUtils.rm_r(dir)
241
+ rescue
242
+ # turn the error message into something the user will understand
243
+ raise "failed to remove tmp merge directory: #{dir}"
244
+ end
240
245
  end
241
246
 
242
247
 
@@ -22,7 +22,7 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  #
24
24
  ################################################################################
25
- module SC
25
+ module SvnAuto
26
26
  ################################################################################
27
27
  class Repository
28
28
  ################################################################################
@@ -46,7 +46,7 @@ module SC
46
46
  def self.ask (config=nil)
47
47
  options = {}
48
48
 
49
- options[:name] = Constants::TERMINAL.ask("Repository Name (used with sc -r): ")
49
+ options[:name] = Constants::TERMINAL.ask("Repository Name (used with sva -r): ")
50
50
 
51
51
  options[:url] = Constants::TERMINAL.ask("Repository URL: ") do |question|
52
52
  if config
@@ -22,26 +22,24 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  #
24
24
  ################################################################################
25
- module SC
25
+ module SvnAuto
26
26
  class Svn
27
27
  ################################################################################
28
28
  # Since we're using regexes, we can only support one language, so we pick
29
29
  # English. This setting forces svn to output English.
30
30
  ENV['LC_MESSAGES'] = 'C'
31
+ ENV['LC_ALL'] = 'en_US'
31
32
 
32
33
  ################################################################################
33
34
  # Run a svn subcommand
34
- def self._svn (*args)
35
+ def self._svn (*args, &block)
35
36
  subcommand = caller(0)[0].sub(/^.*:in\s`([^']+).*$/, '\1')
36
- shell_line = "#{subcommand} #{args.join(' ')}"
37
37
 
38
+ shell_line = "#{subcommand} #{args.join(' ')}"
38
39
  Constants::TERMINAL.say(Constants::TERMINAL.color("svn #{shell_line}", :yellow)) if $DEBUG
39
40
 
40
- if block_given?
41
- `svn #{shell_line} 2>&1`.each {|line| yield(line)}
42
- else
43
- system("svn #{shell_line}")
44
- end
41
+ block ||= lambda {|line| print(line)}
42
+ subprocess('svn', subcommand, *args, &block)
45
43
 
46
44
  if $? != 0 and !['info', 'list', 'merge'].include?(subcommand)
47
45
  raise "svn command failed: #{shell_line}"
@@ -60,11 +58,28 @@ module SC
60
58
  true
61
59
  end
62
60
 
61
+ ################################################################################
62
+ # get the current revision number from the given path
63
+ def self.current_revision (path)
64
+ rev = nil
65
+
66
+ Svn.info(path) do |line|
67
+ m = line.match(/^Revision:\s+(\d+)$/) and rev = m[1].to_i
68
+ end
69
+
70
+ rev
71
+ end
72
+
63
73
  ################################################################################
64
74
  # create a branch using svn copy
65
- def self.branch (project, source, dest)
75
+ def self.branch (project, source, dest, options={})
66
76
  return if self.has_path(dest)
67
77
 
78
+ configuration = {
79
+ :revision => 'HEAD',
80
+
81
+ }.update(options)
82
+
68
83
  relative_dest = dest.sub("#{project.url}/", '')
69
84
  dest_dirs = relative_dest.split(/\//)
70
85
  dest_dirs.pop # don't need the last one
@@ -77,7 +92,8 @@ module SC
77
92
  self.mkdir('-m', "'#{Constants::ME}: creating #{branch_or_tag} path #{dest_path}'", dest_path) unless self.has_path(dest_path)
78
93
  end
79
94
 
80
- self.copy('-m', "'#{Constants::ME}: creating #{branch_or_tag} #{relative_dest}'", source, dest)
95
+ self.copy('-m', "'#{Constants::ME}: creating #{branch_or_tag} #{relative_dest}'",
96
+ '-r', configuration[:revision], source, dest)
81
97
  end
82
98
 
83
99
  ################################################################################
@@ -96,6 +112,31 @@ module SC
96
112
  end
97
113
  end
98
114
 
115
+ ################################################################################
116
+ private
117
+
118
+ ################################################################################
119
+ def self.subprocess (*args, &block)
120
+ tmp = Tempfile.new(Constants::ME)
121
+ stdout = $stdout.dup
122
+ stderr = $stderr.dup
123
+
124
+ begin
125
+ $stdout.reopen(tmp)
126
+ $stderr.reopen(tmp)
127
+ system(*args)
128
+ ensure
129
+ $stdout.reopen(stdout)
130
+ $stderr.reopen(stderr)
131
+ end
132
+
133
+ tmp.close
134
+ IO.foreach(tmp.path, &block)
135
+
136
+ ensure
137
+ File.unlink(tmp.path) if tmp
138
+ end
139
+
99
140
  end
100
141
  end
101
142
  ################################################################################
@@ -0,0 +1,187 @@
1
+ ################################################################################
2
+ #
3
+ # Copyright (C) 2006 Peter J Jones (pjones@pmade.com)
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #
24
+ ################################################################################
25
+ require 'fileutils'
26
+ ################################################################################
27
+ module SvnAuto
28
+ ################################################################################
29
+ class SvnExternals
30
+ ################################################################################
31
+ SVN_EXT = 'svn:externals'
32
+ SVNAUTO_LOCKED = 'sc:locked'
33
+
34
+ ################################################################################
35
+ attr_reader :unlocked
36
+ attr_reader :locked
37
+
38
+ ################################################################################
39
+ def initialize (directories=[], recursive=true)
40
+ if directories.empty?
41
+ Dir.foreach('.') {|f| directories << f if f[0,1] != '.' and File.directory?(f)}
42
+ end
43
+
44
+ fetch_unlocked(directories, recursive)
45
+ fetch_locked(directories, recursive)
46
+ end
47
+
48
+ ################################################################################
49
+ def lock
50
+ affected_parents = Set.new
51
+
52
+ @unlocked.each do |external|
53
+ affected_parents << File.dirname(external.path)
54
+ @locked << external
55
+
56
+ FileUtils.rm_r(external.path)
57
+ Svn.export('-r', external.revision, external.url, external.path)
58
+ Svn.add(external.path)
59
+ end
60
+
61
+ @unlocked.clear
62
+ update_properties_for(affected_parents)
63
+
64
+ short_parents = affected_parents.to_a
65
+ Svn.commit('-m', "#{Constants::ME}: updated lock properties for #{short_parents.join(', ')}", *short_parents)
66
+ end
67
+
68
+ ################################################################################
69
+ def unlock
70
+ affected_parents = Set.new
71
+
72
+ @locked.each do |external|
73
+ affected_parents << File.dirname(external.path)
74
+ @unlocked << external
75
+ Svn.delete(external.path)
76
+ Svn.commit('-m', "#{Constants::ME}: switch #{external.path} from locked to unlocked", external.path)
77
+ FileUtils.rm_rf(external.path)
78
+ end
79
+
80
+ @locked.clear
81
+ update_properties_for(affected_parents)
82
+ short_parents = affected_parents.to_a
83
+
84
+ Svn.update(*short_parents)
85
+ Svn.commit('-m', "#{Constants::ME}: updated lock properties for #{short_parents.join(', ')}", *short_parents)
86
+ end
87
+
88
+ ################################################################################
89
+ private
90
+
91
+ ################################################################################
92
+ def fetch_unlocked (directories, recursive)
93
+ @unlocked = []
94
+ directories.each {|d| property_lookup(d, recursive, SVN_EXT) {|e| @unlocked << e}}
95
+ end
96
+
97
+ ################################################################################
98
+ def fetch_locked (directories, recursive)
99
+ @locked = []
100
+ directories.each {|d| property_lookup(d, recursive, SVNAUTO_LOCKED) {|e| @locked << e}}
101
+ end
102
+
103
+ ################################################################################
104
+ def property_lookup (dir, recursive, property, &block)
105
+ # properties will be set on the parent directory
106
+ parent = File.dirname(dir)
107
+ base = File.basename(dir)
108
+
109
+ @property_cache ||= {}
110
+ @property_cache[property] ||= {}
111
+
112
+ unless @property_cache[property].has_key?(parent)
113
+ @property_cache[property][parent] = {}
114
+
115
+ Svn.propget(property, parent) do |line|
116
+ next if line.match(/^\s*$/)
117
+
118
+ if m = line.match(/^\s*(\S+)\s+-r\s*(\d+)\s+(.+)\s*$/)
119
+ path = File.join(parent, m[1])
120
+ @property_cache[property][parent][m[1]] = SvnInfo.new(:path => path, :revision => m[2], :url => m[3])
121
+ elsif m = line.match(/^\s*(\S+)\s+(.+)\s*$/)
122
+ path = File.join(parent, m[1])
123
+ @property_cache[property][parent][m[1]] = SvnInfo.for(path, true)
124
+ else
125
+ raise "can't parse #{property} entry from #{parent}: #{line.chomp}"
126
+ end
127
+ end
128
+ end
129
+
130
+ if @property_cache[property][parent].has_key?(base)
131
+ yield(@property_cache[property][parent][base])
132
+ elsif recursive
133
+ # we don't recurse into externals because when we lock the parent
134
+ # external, it will lock any sub directories that are also externals
135
+ directories = []
136
+
137
+ Dir.foreach(dir) do |file|
138
+ next if file[0,1] == '.'
139
+ name = File.join(dir, file)
140
+ directories << name if File.directory?(name)
141
+ end
142
+
143
+ directories.each {|d| property_lookup(d, recursive, property, &block)}
144
+ end
145
+ end
146
+
147
+ ################################################################################
148
+ def update_properties_for (parents)
149
+ parents.each do |dir|
150
+ set_properties_for(dir, SVN_EXT, generate_properties_for(dir, SVN_EXT, false, @unlocked, @locked))
151
+ set_properties_for(dir, SVNAUTO_LOCKED, generate_properties_for(dir, SVNAUTO_LOCKED, true, @locked, @unlocked))
152
+ end
153
+ end
154
+
155
+ ################################################################################
156
+ def generate_properties_for (dir, property, with_revision, includes, excludes)
157
+ output_set = includes.select {|i| File.dirname(i.path) == dir}
158
+ result = ""
159
+
160
+ @property_cache[property][dir].values.each do |external|
161
+ output_set << external unless excludes.include?(external)
162
+ end
163
+
164
+ output_set.uniq.each do |external|
165
+ result << File.basename(external.path) << " "
166
+ result << "-r#{external.revision} " if with_revision
167
+ result << external.url << "\n"
168
+ end
169
+
170
+ result
171
+ end
172
+
173
+ ################################################################################
174
+ def set_properties_for (dir, property, data)
175
+ begin
176
+ file = Tempfile.new('sc-property-setting')
177
+ file << data
178
+ file.close
179
+ Svn.propset('--file', file.path, property, dir)
180
+ ensure
181
+ file.unlink
182
+ end
183
+ end
184
+
185
+ end
186
+ end
187
+ ################################################################################