svnauto 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,40 @@
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 SC
26
+ module Constants
27
+ ################################################################################
28
+ # The version number for this copy of SC
29
+ VERSION = '1.0.0'
30
+
31
+ ################################################################################
32
+ # What to call myself
33
+ ME = 'sc'
34
+
35
+ ################################################################################
36
+ TERMINAL = HighLine.new
37
+
38
+ end
39
+ end
40
+ ################################################################################
@@ -0,0 +1,189 @@
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 'abbrev'
26
+ require 'optparse'
27
+ ################################################################################
28
+ module SC
29
+ ################################################################################
30
+ class Dispatcher
31
+ ################################################################################
32
+ def initialize
33
+ @options = OptionParser.new
34
+ @command_names = {}
35
+ @first_names = {}
36
+
37
+ Command.commands.each do |command|
38
+ names = []
39
+
40
+ case command.name
41
+ when Array : command.name.each {|n| names << n.to_s}
42
+ else names << command.name.to_s
43
+ end
44
+
45
+ @first_names[names.first] = command
46
+
47
+ names.each do |name|
48
+ raise "Duplicate name: #{name}" if @command_names.has_key?(name)
49
+ @command_names[name] = command
50
+ end
51
+ end
52
+
53
+ @abbrevations = @command_names.keys.abbrev
54
+ end
55
+
56
+ ################################################################################
57
+ def run (args=ARGV)
58
+ @options.on("-h", "--help", 'This message') do |o|
59
+ list
60
+ end
61
+
62
+ # it's unfortunate that this is a class method
63
+ HighLine.use_color = false
64
+
65
+ config = ConfigFile.new
66
+ HighLine.use_color = true if config[:color]
67
+
68
+ @options.on('--version', "Show #{Constants::ME} version info") do |o|
69
+ Constants::TERMINAL.say(Constants::TERMINAL.color(Constants::VERSION, :red))
70
+ exit
71
+ end
72
+
73
+ @options.on('--verbose', 'Show svn commands as they execute') do |o|
74
+ $DEBUG = true
75
+ end
76
+
77
+ @options.on('--color', 'Use colored output') do |o|
78
+ HighLine.use_color = true
79
+ end
80
+
81
+ @options.on('--no-color', "Don't use colored output") do |o|
82
+ HighLine.use_color = false
83
+ end
84
+
85
+ @project = Project.from_cwd(config)
86
+ ask_for_repository = nil
87
+ ask_for_project = nil
88
+
89
+ @options.on("-p", "--project NAME", "use project NAME or ':' to prompt") do |name|
90
+ if name == ':'
91
+ ask_for_project = true
92
+ else
93
+ @project.name = name
94
+ end
95
+ end
96
+
97
+ @options.on("-r", "--repository NAME", "use repository NAME or ':' to prompt") do |name|
98
+ if name == ':'
99
+ ask_for_repository = true
100
+ elsif (@project.repository = config.find_repository(name)).nil?
101
+ raise "#{name}: no such repository defined"
102
+ end
103
+ end
104
+
105
+ subcommand_options = @options.order!(args)
106
+ klass = klass_from_string(subcommand_options.shift) or list
107
+
108
+ if ask_for_repository or (@project.repository.nil? and klass.without_repository.nil?)
109
+ @project.repository = config.prompt_for_repository
110
+
111
+ if @project.repository.nil?
112
+ raise "please specify a repository, or run from an existing checkout"
113
+ end
114
+ end
115
+
116
+ if ask_for_project or (@project.name.nil? and klass.without_project.nil?)
117
+ @project.prompt_for_name
118
+
119
+ if @project.name.nil?
120
+ raise "please specify a project name, or run from an existing checkout"
121
+ end
122
+ end
123
+
124
+ process(klass, subcommand_options, @project)
125
+ end
126
+
127
+ ################################################################################
128
+ def list (with_exit=true)
129
+ lines = @options.to_s.split(/\r?\n/)
130
+ lines.first << " command [options]"
131
+ puts lines.join("\n")
132
+ puts
133
+ puts "Avaliable Commands:"
134
+
135
+ length = @first_names.keys.inject(0) {|m,o| m >= o.length ? m : o.length} + 4
136
+ @first_names.keys.sort.each do |name|
137
+ puts name.rjust(length) + " " + @command_names[name].description
138
+ end
139
+
140
+ exit if with_exit
141
+ end
142
+
143
+ ################################################################################
144
+ def process (klass, options, project)
145
+ klass.options.on('-h', '--help', 'This message') do |val|
146
+ puts usage_for(klass)
147
+ exit
148
+ end
149
+
150
+ klass.reset!
151
+ command_extras = klass.options.order!(options)
152
+ return unless klass.instance_methods.include?('run')
153
+
154
+ if klass.args_min and command_extras.length < klass.args_min
155
+ puts usage_for(klass)
156
+ exit 1
157
+ elsif klass.args_max and command_extras.length > klass.args_max
158
+ puts usage_for(klass)
159
+ exit 1
160
+ end
161
+
162
+ klass.new.run(project, command_extras)
163
+ end
164
+
165
+ ################################################################################
166
+ def usage_for (klass)
167
+ name = klass.name.is_a?(Array) ? klass.name.first : klass.name
168
+
169
+ lines = klass.options.to_s.split(/\r?\n/)
170
+ lines.first << " #{name} #{klass.usage || '[options]'}"
171
+ lines.first << "\n#{klass.description}"
172
+
173
+ if klass.name.is_a?(Array) and klass.name.length > 1
174
+ lines.first << "\nShortcuts: #{klass.name[1..-1].join(', ')}"
175
+ end
176
+
177
+ lines.first << "\n\nOptions for the #{name} command:"
178
+ lines.join("\n")
179
+ end
180
+
181
+ ################################################################################
182
+ def klass_from_string (string)
183
+ @command_names[@abbrevations[string]]
184
+ end
185
+
186
+ end
187
+ ################################################################################
188
+ end
189
+ ################################################################################
@@ -0,0 +1,51 @@
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 SC
26
+ ################################################################################
27
+ module Path
28
+ ################################################################################
29
+ # convert a relative path to home to 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
+ path[0, 1] == '/' ? 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
+ path[0, home.length] == home ? path.sub(home, '~') : path
46
+ end
47
+
48
+ end
49
+ ################################################################################
50
+ end
51
+ ################################################################################
@@ -0,0 +1,265 @@
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 SC
26
+ ################################################################################
27
+ class Project
28
+ ################################################################################
29
+ # The name of the project
30
+ attr_accessor :name
31
+
32
+ ################################################################################
33
+ # The repository for this project
34
+ attr_accessor :repository
35
+
36
+ ################################################################################
37
+ # Try to get info about the current directory's repository
38
+ def self.from_cwd (config)
39
+ url = nil
40
+ project = nil
41
+
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
47
+
48
+ if url and project
49
+ # try to find a matching repository
50
+ repository = config[:repositories].find {|r| r.url == url}
51
+ repository ||= Repository.new(:url => url)
52
+
53
+ new({
54
+ :repository => repository,
55
+ :name => project[url.length + 1 .. -1].sub(/\/.*$/, ''),
56
+ })
57
+ else
58
+ new
59
+ end
60
+ end
61
+
62
+ ################################################################################
63
+ # remove special characters from a project name
64
+ def self.clean_name (non_clean_name)
65
+ non_clean_name.gsub(/[^\w\d_-]+/, '_')
66
+ end
67
+
68
+ ################################################################################
69
+ # Setup a new project
70
+ def initialize (options={})
71
+ @name = options[:name]
72
+ @repository = options[:repository]
73
+ end
74
+
75
+ ################################################################################
76
+ # get the URL to this project
77
+ def url
78
+ "#{@repository.url}/#{@name}"
79
+ end
80
+
81
+ ################################################################################
82
+ # convert to a string
83
+ def to_s
84
+ @name
85
+ end
86
+
87
+ ################################################################################
88
+ # prompt the user to pick a project name from the repository
89
+ def prompt_for_name
90
+ names = []
91
+
92
+ if URI.parse(@repository.url).scheme != 'file'
93
+ puts "Fetching project list from repository, please wait..."
94
+ $stdout.flush
95
+ end
96
+
97
+ Svn.list(@repository.url) do |line|
98
+ next if line.match(/^svn:/)
99
+ names << line.sub(/\/$/, '').strip
100
+ end
101
+
102
+ if names.empty?
103
+ raise "there are no projects in the repository #{@repository.url}"
104
+ end
105
+
106
+ @name = Constants::TERMINAL.choose do |menu|
107
+ menu.header = "Projects"
108
+ menu.prompt = "Please choose a project: "
109
+
110
+ names.each do |name|
111
+ menu.choice(name) {name}
112
+ end
113
+ end
114
+ end
115
+
116
+ ################################################################################
117
+ # get a list of the directories that sc uses
118
+ def directories
119
+ [
120
+ url,
121
+ trunk,
122
+ branches,
123
+ branches('rel'),
124
+ branches('bug'),
125
+ branches('exp'),
126
+ tags,
127
+ tags('rel'),
128
+ tags('bug'),
129
+ tags('exp'),
130
+ ]
131
+ end
132
+
133
+ ################################################################################
134
+ # create the necessary project directories in the repository
135
+ def create
136
+ directories.each do |dir|
137
+ unless Svn.has_path(dir)
138
+ Svn.mkdir('-m', "'#{Constants::ME}: creating #{dir} for project #{name}.'", dir)
139
+ end
140
+ end
141
+ end
142
+
143
+ ################################################################################
144
+ # get the latest release branch version
145
+ def latest_release_branch
146
+ @versions ||= []
147
+
148
+ if @versions.empty?
149
+ Svn.list(release) do |line|
150
+ next if line.match(/^svn:/)
151
+ @versions << Version.new(line.sub(/\/$/, '').strip)
152
+ end
153
+ end
154
+
155
+ @versions.sort.last
156
+ end
157
+
158
+ ################################################################################
159
+ # get the latest release tag for the given branch version
160
+ def latest_release_tag (branch_version)
161
+ tags = []
162
+ branch_version_str = branch_version.major_minor
163
+
164
+ Svn.list(tags('rel')) do |line|
165
+ tag = line.sub(/\/$/, '').strip
166
+ next unless tag[0, branch_version_str.length] == branch_version_str
167
+ tags << Version.new(tag)
168
+ end
169
+
170
+ tags.sort.last
171
+ end
172
+
173
+ ################################################################################
174
+ # checkout to the workspace
175
+ def checkout (version=nil, to=nil)
176
+ workspace = @repository.workspace
177
+ url = trunk
178
+ co_name = "#{@name}-trunk"
179
+
180
+ Dir.mkdir(workspace) unless File.exist?(workspace)
181
+
182
+ case version
183
+ when String
184
+ url = version
185
+ co_name = "#{@name}-" + version.sub("#{self.url}/", '').gsub(/\//, '-')
186
+ co_name.sub!(/branches-/, '')
187
+ co_name.sub!(/releases/, 'rel')
188
+ when Version
189
+ url = version.macro.nil? ? release(version.to_s) : tags("rel/#{version.to_s}")
190
+ co_name = "#{@name}-rel-#{version}"
191
+ end
192
+
193
+ co_name = to if to
194
+ local_dir = File.join(workspace, co_name)
195
+ Svn.checkout(url, local_dir) unless File.exist?(local_dir)
196
+
197
+ local_dir
198
+ end
199
+
200
+ ################################################################################
201
+ # do some common stuff for merging
202
+ def merge (version, start_tag, end_tag, &error_block)
203
+ tmp_checkout = "#{@name}-merge-tmp-#{$$}"
204
+ dir = self.checkout(version, tmp_checkout)
205
+
206
+ base = self.url
207
+ start_tag = self.tags(start_tag) unless start_tag[0, base.length] == base
208
+ end_tag = self.tags(end_tag) unless end_tag[0, base.length] == base
209
+
210
+ Dir.chdir(dir) do
211
+ conflicts = false
212
+
213
+ Svn.merge(start_tag, end_tag) do |line|
214
+ conflicts = true if line.match(/^\s*C/)
215
+ print line
216
+ end
217
+
218
+ if conflicts
219
+ message = "merge failed, you need to resolve conflicts in #{dir} "
220
+ message << yield if block_given?
221
+ raise message
222
+ end
223
+
224
+ Svn.commit('-m', "'#{Constants::ME}: merging branch using tags #{start_tag} and #{end_tag}'")
225
+ end
226
+
227
+ # clean up after merge
228
+ system('rm', '-rf', dir) or raise "failed to remove tmp merge directory: #{dir}"
229
+ end
230
+
231
+
232
+ ################################################################################
233
+ # get the full URL to the trunk for this repository and project
234
+ def trunk
235
+ "#{url}/trunk"
236
+ end
237
+
238
+ ################################################################################
239
+ # get the full URL path for branches
240
+ def branches (subdir=nil)
241
+ path = "#{url}/branches"
242
+ path << "/#{subdir}" if subdir
243
+ path
244
+ end
245
+
246
+ ################################################################################
247
+ # get the full URL path for tags
248
+ def tags (subdir=nil)
249
+ path = "#{url}/tags"
250
+ path << "/#{subdir}" if subdir
251
+ path
252
+ end
253
+
254
+ ################################################################################
255
+ # get the full URL to the release branch
256
+ def release (version=nil)
257
+ path = branches('rel')
258
+ path << "/#{version}" if version
259
+ path
260
+ end
261
+
262
+ end
263
+ ################################################################################
264
+ end
265
+ ################################################################################