svnauto 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ ################################################################################