svnbranch 0.2.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.
- data/History.txt +47 -0
- data/License.txt +22 -0
- data/Manifest.txt +26 -0
- data/README.txt +74 -0
- data/Rakefile +23 -0
- data/TODO.txt +3 -0
- data/bin/svnbranch +848 -0
- data/lib/svnbranch.rb +56 -0
- data/man1/Makefile +19 -0
- data/man1/svnbranch.1 +526 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/svnbranch_spec.rb +8 -0
- data/tasks/ann.rake +81 -0
- data/tasks/bones.rake +21 -0
- data/tasks/gem.rake +126 -0
- data/tasks/git.rake +41 -0
- data/tasks/manifest.rake +49 -0
- data/tasks/notes.rake +28 -0
- data/tasks/post_load.rake +39 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +268 -0
- data/tasks/spec.rake +55 -0
- data/tasks/svn.rake +48 -0
- data/tasks/test.rake +38 -0
- data/test/test_svnbranch.rb +0 -0
- metadata +92 -0
data/bin/svnbranch
ADDED
@@ -0,0 +1,848 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
# -*- Ruby -*-
|
3
|
+
#
|
4
|
+
# PURPOSE: Manage Subversion branches with ease.
|
5
|
+
#
|
6
|
+
# USAGE:
|
7
|
+
# See man page, or type 'svnbranch help'
|
8
|
+
#
|
9
|
+
# REQUIREMENT:
|
10
|
+
# RubyGems gem
|
11
|
+
# Watcher gem
|
12
|
+
# Common Subversion repository naming conventions, i.e.:
|
13
|
+
# repos/project/trunk/<main code here>
|
14
|
+
# repos/project/branches/<optional sub-branch>/<branch-name>/<branch-code>
|
15
|
+
# repos/project/tags/<optional sub-branch>/<branch-name>-PRE/<branch-code>
|
16
|
+
# repos/project/tags/<optional sub-branch>/<branch-name>-POST/<branch-code>
|
17
|
+
#
|
18
|
+
# Created by Karrick McDermott on 2008-07-31.
|
19
|
+
# Copyright (c) 2008 All rights reserved.
|
20
|
+
######################################################################
|
21
|
+
|
22
|
+
# Standard Ruby Installation
|
23
|
+
require 'fileutils'
|
24
|
+
require 'open3'
|
25
|
+
|
26
|
+
# Third-party Gems
|
27
|
+
require 'rubygems'
|
28
|
+
require 'watcher'
|
29
|
+
|
30
|
+
require File.expand_path(
|
31
|
+
File.join(File.dirname(__FILE__), '..', 'lib', 'svnbranch'))
|
32
|
+
|
33
|
+
######################################################################
|
34
|
+
# CONSTANTS
|
35
|
+
RSYNC_OPTS = "-a --delete --delete-excluded --exclude '*~' --exclude '#*#'"
|
36
|
+
SSH_OPTS='-q -o PasswordAuthentication=no -o StrictHostKeyChecking=no -o ConnectTimeout=2'
|
37
|
+
|
38
|
+
######################################################################
|
39
|
+
def branch_exists? (repos, branch)
|
40
|
+
# NOTE: This works for any subdirectory on the Subversion repository,
|
41
|
+
# including the trunk, branches, and tags.
|
42
|
+
|
43
|
+
# assume branch exists
|
44
|
+
branch_exists = true
|
45
|
+
|
46
|
+
$watcher.verbose("Looking for repository branch [#{branch}]") do
|
47
|
+
# Use begin / rescue rather than Watcher in order to stifle expected
|
48
|
+
# error messages that occur if branch doesn't exist.
|
49
|
+
begin
|
50
|
+
subversion("svn ls #{repos}/#{branch}")
|
51
|
+
rescue
|
52
|
+
# execute will throw if branch missing
|
53
|
+
# If here, then branch was not found
|
54
|
+
branch_exists = false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return branch_exists
|
59
|
+
end
|
60
|
+
|
61
|
+
######################################################################
|
62
|
+
def close_branch! (force = false)
|
63
|
+
# Example: We are in a branch other than the trunk, and decide to
|
64
|
+
# call svnbranch, when work is perceived complete on a branch:
|
65
|
+
# $ svnbranch close
|
66
|
+
|
67
|
+
branch_path = nil
|
68
|
+
repos = nil
|
69
|
+
post_tag = nil
|
70
|
+
|
71
|
+
$watcher.verbose("Checking work environment") do
|
72
|
+
current_dir = inspect_current_work_directory
|
73
|
+
repos = current_dir[:repos]
|
74
|
+
$watcher.debug("repository = [#{repos}]")
|
75
|
+
# "https://svn.example.com/svn/project_name"
|
76
|
+
branch_path = current_dir[:branch]
|
77
|
+
$watcher.debug("branch_path = [#{branch_path}]")
|
78
|
+
# "trunk"
|
79
|
+
# "branches/bug-fixes/BUG-1234"
|
80
|
+
|
81
|
+
if branch_path == 'trunk'
|
82
|
+
msg = "Cannot close a branch when you are in the trunk"
|
83
|
+
raise RuntimeError.new(msg)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Check for Subversion commit in this directory
|
87
|
+
verify_subversion_commit(Dir.getwd())
|
88
|
+
|
89
|
+
# NOTE: Expect branch_path like below:
|
90
|
+
# branches/bug-fixes/BUG-1234
|
91
|
+
branch_path_split = branch_path.split(/\//)
|
92
|
+
branch_name = branch_path_split.pop # capture the right-most element
|
93
|
+
branch_path_split.shift # discard the prefix (branches | tags)
|
94
|
+
sub_branch = branch_path_split.join('/') # reassemble
|
95
|
+
|
96
|
+
# If sub-branch empty, then no sub-branch
|
97
|
+
sub_branch = '/' + sub_branch unless sub_branch == ''
|
98
|
+
|
99
|
+
pre_tag = "tags#{sub_branch}/#{branch_name}-PRE"
|
100
|
+
post_tag = "tags#{sub_branch}/#{branch_name}-POST"
|
101
|
+
|
102
|
+
msg = "Verifying branch exists [#{branch_path}]"
|
103
|
+
$watcher.verbose(msg) do
|
104
|
+
[branch_path, pre_tag].each do |b|
|
105
|
+
unless branch_exists?(repos, b)
|
106
|
+
msg = "Resource missing [#{b}] on Subversion server;"
|
107
|
+
raise RuntimeError.new(msg)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
msg = "Verifying branch not yet closed [#{branch_path}]"
|
113
|
+
$watcher.verbose(msg) do
|
114
|
+
if branch_exists?(repos, post_tag)
|
115
|
+
if force
|
116
|
+
remove_branch!(repos, post_tag)
|
117
|
+
else
|
118
|
+
msg = "Resource already exists [#{post_tag}];"
|
119
|
+
msg << " use '-f' to force overwrite"
|
120
|
+
raise RuntimeError.new(msg)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Tag post-work for branch for later merge purposes
|
127
|
+
tag_branch!(repos, branch_path, post_tag)
|
128
|
+
end
|
129
|
+
|
130
|
+
######################################################################
|
131
|
+
def command_exists? (cmd)
|
132
|
+
# determine whether the [cmd] utility exists on system
|
133
|
+
command_exists = false
|
134
|
+
|
135
|
+
$watcher.verbose("Looking for [#{cmd}] utilitiy on system") do
|
136
|
+
# Use begin / rescue rather than Watcher in order to stifle expected
|
137
|
+
# error messages that occur if branch doesn't exist.
|
138
|
+
begin
|
139
|
+
command_exists = execute("which #{cmd}").first.strip
|
140
|
+
rescue
|
141
|
+
# execute will throw if branch missing
|
142
|
+
# If here, then branch was not found
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
return command_exists
|
147
|
+
end
|
148
|
+
|
149
|
+
######################################################################
|
150
|
+
def copy_branch! (repos, existing_branch, new_branch)
|
151
|
+
msg = "Copying Subversion branch [#{existing_branch}]"
|
152
|
+
msg << " to [#{new_branch}]"
|
153
|
+
$watcher.verbose(msg) do
|
154
|
+
existing_branch = repos + '/' + existing_branch
|
155
|
+
new_branch = repos + '/' + new_branch
|
156
|
+
cmd = "svn copy -m \"#{msg}\" #{existing_branch} #{new_branch}"
|
157
|
+
# NOTE: Not a 'safe' command because modifies server
|
158
|
+
subversion(cmd, false)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
######################################################################
|
163
|
+
def copy_dir! (src, dst)
|
164
|
+
# src -- source directory for copy
|
165
|
+
# dst -- destination directory for copy
|
166
|
+
|
167
|
+
src = File.expand_path(src)
|
168
|
+
dst = File.expand_path(dst)
|
169
|
+
parent = File.expand_path(File.join(dst,'..'))
|
170
|
+
|
171
|
+
msg = "Verifying parent directory [#{parent}] exists"
|
172
|
+
$watcher.debug(msg) do
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
msg = "Copying working directory to new branch directory"
|
177
|
+
$watcher.debug(msg) do
|
178
|
+
# Use [rsync] if available (much more efficient)
|
179
|
+
if command_exists?('rsync') == false
|
180
|
+
copy_dir_with_ruby!(src, dst)
|
181
|
+
else
|
182
|
+
copy_dir_with_rsync!(src, dst)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
######################################################################
|
188
|
+
def copy_dir_with_rsync! (src, dst)
|
189
|
+
# src -- source directory for copy
|
190
|
+
# dst -- destination directory for copy
|
191
|
+
|
192
|
+
# Note: No need to remove existsing directory or files because
|
193
|
+
# rsync will take care of it.
|
194
|
+
|
195
|
+
msg = "Copying data from [#{src}] to [#{dst}] using Rsync"
|
196
|
+
$watcher.verbose(msg) do
|
197
|
+
cmd = "rsync #{RSYNC_OPTS} #{src}/ #{dst}"
|
198
|
+
execute(cmd)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
######################################################################
|
203
|
+
def copy_dir_with_ruby! (src, dst)
|
204
|
+
# src -- source directory for copy
|
205
|
+
# dst -- destination directory for copy
|
206
|
+
|
207
|
+
# Must remove dst if present
|
208
|
+
if File.exists?(dst)
|
209
|
+
$watcher.verbose("Removing previous directory [#{dst}]") do
|
210
|
+
#FileUtils.remove_entry_secure(dst)
|
211
|
+
FileUtils.remove_entry(dst)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
msg = "Copying data from [#{src}] to [#{dst}] using Ruby"
|
216
|
+
$watcher.verbose(msg) do
|
217
|
+
# FileUtils.cp_r(src, dst)
|
218
|
+
FileUtils.copy_entry(src, dst, preserve = true)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
######################################################################
|
223
|
+
# create_branch!
|
224
|
+
#
|
225
|
+
# force -- forces creation of new branch regardless of uncommitted changes
|
226
|
+
# new_dir -- true => create a local copy of branch
|
227
|
+
# false => re-use current working directory
|
228
|
+
######################################################################
|
229
|
+
def create_branch! (new_branch_name, force, new_dir = true)
|
230
|
+
# Example: We are in trunk, and decide call svnbranch, as below:
|
231
|
+
# $ svnbranch create bug-fixes/BUG-1234
|
232
|
+
|
233
|
+
# new_branch_name -- bug-fixes/BUG-1234
|
234
|
+
|
235
|
+
|
236
|
+
start_dir = File.expand_path(Dir.getwd())
|
237
|
+
if new_dir == true
|
238
|
+
parent_dir = File.join(start_dir, '..')
|
239
|
+
$watcher.debug("parent_dir = [#{parent_dir}]")
|
240
|
+
new_dir = File.expand_path(File.join(parent_dir, 'branches', new_branch_name))
|
241
|
+
$watcher.debug("new_dir = [#{new_dir}]")
|
242
|
+
verify_path!(File.expand_path(File.join(new_dir,'..')))
|
243
|
+
end
|
244
|
+
|
245
|
+
# new_branch_path -- branches/bug-fixes/BUG-1234
|
246
|
+
# new_pre_tag -- tags/bug-fixes/BUG-1234-PRE
|
247
|
+
# new_post_tag -- tags/bug-fixes/BUG-1234-POST
|
248
|
+
new_branch_path = "branches/#{new_branch_name}"
|
249
|
+
new_pre_tag = "tags/#{new_branch_name}-PRE"
|
250
|
+
new_post_tag = "tags/#{new_branch_name}-POST"
|
251
|
+
|
252
|
+
# parent_branch_path
|
253
|
+
# if no sub-branches -> ""
|
254
|
+
# if sub-branches -> "pat/experimental"
|
255
|
+
path_array = new_branch_path.split(/\//)
|
256
|
+
path_array.pop
|
257
|
+
parent_branch_path = path_array.join('/')
|
258
|
+
$watcher.debug("parent_branch_path = [#{parent_branch_path}]")
|
259
|
+
path_array.shift
|
260
|
+
parent_tags_path = path_array.unshift('tags').join('/')
|
261
|
+
$watcher.debug("parent_tags_path = [#{parent_tags_path}]")
|
262
|
+
|
263
|
+
repos = nil
|
264
|
+
working_branch = nil
|
265
|
+
|
266
|
+
$watcher.verbose("Checking work environment") do
|
267
|
+
if new_branch_name == 'trunk'
|
268
|
+
msg = "Cannot create a trunk branch"
|
269
|
+
raise RuntimeError.new(msg)
|
270
|
+
end
|
271
|
+
|
272
|
+
# repos -- https://svn.example.com/svn/project
|
273
|
+
# working_branch
|
274
|
+
# trunk
|
275
|
+
# pat/experimental
|
276
|
+
current_dir = inspect_current_work_directory
|
277
|
+
repos = current_dir[:repos]
|
278
|
+
$watcher.debug("repository = [#{repos}]")
|
279
|
+
working_branch = current_dir[:branch]
|
280
|
+
$watcher.debug("working_branch = [#{working_branch}]")
|
281
|
+
|
282
|
+
# Verify local changes committed
|
283
|
+
if not subversion_committed?(start_dir) and not force
|
284
|
+
msg = "Files in this branch have not been committed;"
|
285
|
+
msg << " use '-f' for force override"
|
286
|
+
raise RuntimeError.new(msg)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Verify / create branch and sub-branches on repository server
|
290
|
+
verify_branch!(repos, parent_branch_path)
|
291
|
+
verify_branch!(repos, parent_tags_path)
|
292
|
+
|
293
|
+
msg = "Verifying branch [#{new_branch_name}] doesn't already exist"
|
294
|
+
$watcher.verbose(msg) do
|
295
|
+
# Ensure branch, PRE-branch, and POST-branch don't exist
|
296
|
+
[new_branch_path, new_pre_tag, new_post_tag].each do |b|
|
297
|
+
# NOTE: Okay to use [branch_exists?] for a tag...
|
298
|
+
if branch_exists?(repos, b)
|
299
|
+
if force
|
300
|
+
remove_branch!(repos, b)
|
301
|
+
else
|
302
|
+
msg = "Resource already exists [#{b}]; check your"
|
303
|
+
msg << " <new-branch> argument, or use '-f' to force overwrite"
|
304
|
+
raise RuntimeError.new(msg)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Duplicate existing branch on Subversion server
|
312
|
+
copy_branch!(repos, working_branch, new_branch_path)
|
313
|
+
|
314
|
+
# Tag pre-work for branch for later merge purposes
|
315
|
+
tag_branch!(repos, new_branch_path, new_pre_tag)
|
316
|
+
|
317
|
+
if new_dir == false
|
318
|
+
# re-use current working directory
|
319
|
+
switch_branch!(repos, start_dir, new_branch_path)
|
320
|
+
else
|
321
|
+
# When do this?
|
322
|
+
# NOTE: Although it would be prudent to perform this prior to
|
323
|
+
# directory duplication so to prevent merging remote updates
|
324
|
+
# twice, (once for new branch, and once when merging back
|
325
|
+
# with working_branch), there is a small possibility that user
|
326
|
+
# would not want this. THE JURY IS OUT!
|
327
|
+
update_branch!(repos, working_branch) if false
|
328
|
+
|
329
|
+
# Duplicate existing working directory on this host
|
330
|
+
copy_dir!(start_dir, new_dir)
|
331
|
+
|
332
|
+
# Go to new_branch_dir, and tell Subversion to switch it
|
333
|
+
# to new branch
|
334
|
+
switch_branch!(repos, new_dir, new_branch_path)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
######################################################################
|
339
|
+
def delete_branch! (branch_name)
|
340
|
+
# Example: We are in the trunk, and decide to delete all
|
341
|
+
# references to a different branch.
|
342
|
+
# $ svnbranch delete bug-fixes/BUG-1234
|
343
|
+
|
344
|
+
# sub_branch -- bug-fixes
|
345
|
+
# branch_name -- BUG-1234
|
346
|
+
|
347
|
+
$watcher.verbose("Checking work environment") do
|
348
|
+
current_dir = inspect_current_work_directory
|
349
|
+
repos = current_dir[:repos]
|
350
|
+
$watcher.debug("repository = [#{repos}]")
|
351
|
+
# "https://svn.example.com/svn/project_name"
|
352
|
+
|
353
|
+
branch_path = "branches/#{branch_name}"
|
354
|
+
# branches/bug-fixes/BUG-1234
|
355
|
+
pre_tag = "tags/#{branch_name}-PRE"
|
356
|
+
# tags/bug-fixes/BUG-1234-PRE
|
357
|
+
post_tag = "tags/#{branch_name}-POST"
|
358
|
+
# tags/bug-fixes/BUG-1234-POST
|
359
|
+
|
360
|
+
# Use branch_path from method parameter, not working directory
|
361
|
+
$watcher.debug("branch_path = [#{branch_path}]")
|
362
|
+
$watcher.debug("pre_tag = [#{pre_tag}]")
|
363
|
+
$watcher.debug("post_tag = [#{post_tag}]")
|
364
|
+
|
365
|
+
$watcher.verbose("Deleteing branch [#{branch_path}]") do
|
366
|
+
[branch_path, pre_tag, post_tag].each do |b|
|
367
|
+
if branch_exists?(repos, b)
|
368
|
+
msg = "Deleteing resource [#{b}]"
|
369
|
+
$watcher.verbose(msg) do
|
370
|
+
cmd = "svn rm -m \"#{msg}\" #{repos}/#{b}"
|
371
|
+
# NOTE: Not a 'safe' command because modifies server
|
372
|
+
subversion(cmd, false)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
######################################################################
|
381
|
+
# execute -- execute a command
|
382
|
+
def execute (command)
|
383
|
+
std_out = ''
|
384
|
+
std_err = ''
|
385
|
+
|
386
|
+
$watcher.debug('[' + command + ']') do
|
387
|
+
Open3.popen3(command) do |stdin, stdout, stderr|
|
388
|
+
# read from stdout and stderr (IO classes)
|
389
|
+
std_out = stdout.readlines
|
390
|
+
std_err = stderr.readlines
|
391
|
+
end
|
392
|
+
|
393
|
+
# log the output lines if debugging
|
394
|
+
if std_out.class == Array
|
395
|
+
std_out.each { |line| $watcher.debug(line.strip) }
|
396
|
+
else
|
397
|
+
$watcher.debug(std_out.inspect)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# filter out pseudo-terminal errors
|
402
|
+
std_err.delete_if { |e| e =~ /^Pseudo-terminal/ }
|
403
|
+
|
404
|
+
# check for remaining errors
|
405
|
+
if std_err.length > 0
|
406
|
+
# log the output lines if debugging
|
407
|
+
if std_err.class == Array
|
408
|
+
std_err.each { |line| $watcher.debug(line.strip) }
|
409
|
+
else
|
410
|
+
$watcher.debug(std_err.inspect)
|
411
|
+
end
|
412
|
+
raise RuntimeError.new(std_err.join("\n"))
|
413
|
+
end
|
414
|
+
|
415
|
+
# return the output lines as an array
|
416
|
+
std_out
|
417
|
+
end
|
418
|
+
|
419
|
+
######################################################################
|
420
|
+
def inspect_current_work_directory
|
421
|
+
repos = nil
|
422
|
+
url = nil
|
423
|
+
$watcher.verbose("Inspecting current Subversion work directory") do
|
424
|
+
subversion("svn info").each do |line|
|
425
|
+
if line =~ /^URL: (.*)$/
|
426
|
+
url = $1
|
427
|
+
$watcher.debug("Repository URL: #{$1}") if $DEBUG
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
if url == nil
|
432
|
+
msg = "Current directory does not appear to be a Subversion"
|
433
|
+
msg << " working directory"
|
434
|
+
raise RuntimeError.new(msg)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# NOTE: Current branch may be either [trunk] or some different name.
|
439
|
+
# If different name, then it should be a member of the
|
440
|
+
# [branches] subdirectory on the Subversion server.
|
441
|
+
|
442
|
+
# Break up the URL to a repository parent directory and branch name
|
443
|
+
# Example: "https://svn.example.com/svn/project/trunk"
|
444
|
+
# $1 --> "https://svn.example.com/svn/project"
|
445
|
+
# $2 --> "trunk"
|
446
|
+
# Example: "https://svn.example.com/svn/project/branches/bug-fixes/BUG-1234"
|
447
|
+
# $1 --> "https://svn.example.com/svn/project"
|
448
|
+
# $2 --> "branches/bug-fixes/BUG-1234"
|
449
|
+
md = url.match(/(.*)\/(branches\/.*|trunk)/)
|
450
|
+
return {:repos => md[1], :branch => md[2]}
|
451
|
+
end
|
452
|
+
|
453
|
+
######################################################################
|
454
|
+
def man_page
|
455
|
+
found = false
|
456
|
+
$:.each do |path|
|
457
|
+
if not found
|
458
|
+
filename = File.join(path,'..','man1',File.basename($0)+'.1')
|
459
|
+
filename = File.expand_path(filename)
|
460
|
+
$watcher.debug("Looking for [#{filename}]") do
|
461
|
+
if File.exists?(filename)
|
462
|
+
found = true
|
463
|
+
system("man #{filename}")
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
######################################################################
|
471
|
+
def make_repository_path! (repos, branch)
|
472
|
+
$watcher.verbose("Creating repository branch [#{branch}]") do
|
473
|
+
cmd = "svn mkdir --parents"
|
474
|
+
cmd << " -m \"Creating repository sub-branch [#{branch}]\""
|
475
|
+
cmd << " #{repos}/#{branch}"
|
476
|
+
# NOTE: Not a 'safe' command because modifies server
|
477
|
+
subversion(cmd, false)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
######################################################################
|
482
|
+
def merge_branch! (merge_branch)
|
483
|
+
# Example: We are in a branch, and decide to merge in the changes
|
484
|
+
# from a different branch into this directory.
|
485
|
+
# $ svnbranch merge bug-fixes/BUG-1234
|
486
|
+
|
487
|
+
# merge_branch -- bug-fixes/BUG-1234
|
488
|
+
# sub_branch -- bug-fixes
|
489
|
+
# branch_name -- BUG-1234
|
490
|
+
|
491
|
+
repos = nil
|
492
|
+
working_branch = nil
|
493
|
+
merge_branch_path = "branches/#{merge_branch}"
|
494
|
+
# branches/bug-fixes/BUG-1234
|
495
|
+
merge_pre_tag = "tags/#{merge_branch}-PRE"
|
496
|
+
# tags/bug-fixes/BUG-1234-PRE
|
497
|
+
merge_post_tag = "tags/#{merge_branch}-POST"
|
498
|
+
# tags/bug-fixes/BUG-1234-POST
|
499
|
+
|
500
|
+
$watcher.verbose("Checking work environment") do
|
501
|
+
if merge_branch == 'trunk'
|
502
|
+
msg = "Cannot merge the trunk branch into a different branch."
|
503
|
+
raise RuntimeError.new(msg)
|
504
|
+
end
|
505
|
+
|
506
|
+
current_dir = inspect_current_work_directory
|
507
|
+
repos = current_dir[:repos]
|
508
|
+
$watcher.debug("repository = [#{repos}]")
|
509
|
+
# "https://svn.example.com/svn/project_name"
|
510
|
+
working_branch = current_dir[:branch]
|
511
|
+
# "trunk"
|
512
|
+
# "branches/2.1-maint"
|
513
|
+
|
514
|
+
msg = "Verifying branch exists [#{merge_branch_path}]"
|
515
|
+
$watcher.verbose(msg) do
|
516
|
+
[merge_branch_path, merge_pre_tag, merge_post_tag].each do |b|
|
517
|
+
unless branch_exists?(repos, b)
|
518
|
+
msg = "Resource missing [#{b}]; check your"
|
519
|
+
msg << " <merge-branch> argument"
|
520
|
+
raise RuntimeError.new(msg)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Update this Subversion work directory
|
526
|
+
update_branch!(repos, working_branch)
|
527
|
+
end
|
528
|
+
|
529
|
+
# Pull in diff from PRE to POST into this work directory
|
530
|
+
msg = "Merging changes from branch [#{merge_branch}] into "
|
531
|
+
msg << "working directory [#{Dir.getwd()}]"
|
532
|
+
$watcher.verbose(msg) do
|
533
|
+
# NOTE: This gets the diffs between the PRE- and POST- tags, then applies
|
534
|
+
# those changes to the current directory.
|
535
|
+
# NOTE: Because the destination working directory is left off of this
|
536
|
+
# command, Subversion will apply the diffs to the current
|
537
|
+
# directory. It is imperative to be in the working directory when
|
538
|
+
# the below command is invoked.
|
539
|
+
cmd = "svn merge #{repos}/#{merge_pre_tag} #{repos}/#{merge_post_tag}"
|
540
|
+
subversion(cmd)
|
541
|
+
end
|
542
|
+
|
543
|
+
msg = "The changes for [#{merge_branch}] have been merged into the"
|
544
|
+
msg << " working directory [#{Dir.getwd()}]"
|
545
|
+
$watcher.always(msg)
|
546
|
+
$watcher.always("=================================================================")
|
547
|
+
$watcher.always("= NOW BUILD, TEST, AND RESOLVE PROBLEMS BEFORE YOU COMMIT! =")
|
548
|
+
$watcher.always("= (Do *not* re-merge the same merge branch into this directory) =")
|
549
|
+
$watcher.always("= Please add the below line into your next commit log: =")
|
550
|
+
$watcher.always("= \"Merged [#{merge_branch}] into [#{working_branch}].\" =")
|
551
|
+
$watcher.always("=============================================================
|
552
|
+
====")
|
553
|
+
end
|
554
|
+
|
555
|
+
######################################################################
|
556
|
+
# Remove traces of a branch (or tag) if it exists
|
557
|
+
def remove_branch! (repos, branch)
|
558
|
+
if branch_exists?(repos, branch)
|
559
|
+
msg = "Removing Subversion branch [#{branch}]"
|
560
|
+
$watcher.verbose(msg) do
|
561
|
+
cmd = "svn rm -m \"#{msg}\" #{repos}/#{branch}"
|
562
|
+
# NOTE: Not a 'safe' command because modifies server
|
563
|
+
subversion(cmd, false)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
######################################################################
|
569
|
+
# Easily turn off execution of subversion commands while developing
|
570
|
+
# A 'safe' command doesn't change Subversion repository on server.
|
571
|
+
def subversion (command, safe = true)
|
572
|
+
if safe or not $DEBUG
|
573
|
+
return execute(command)
|
574
|
+
else
|
575
|
+
return $watcher.debug("SIMULATE: [#{command}]")
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
######################################################################
|
580
|
+
def subversion_committed? (branch_dir)
|
581
|
+
subversion_committed = false
|
582
|
+
|
583
|
+
msg = "Determining whether changes committed back to branch"
|
584
|
+
msg << " [#{branch_dir}]"
|
585
|
+
$watcher.verbose(msg) do
|
586
|
+
# Use begin / rescue rather than Watcher to stifle expected
|
587
|
+
# error messages when branch doesn't exist.
|
588
|
+
begin
|
589
|
+
cmd = "svn status #{branch_dir}"
|
590
|
+
# status = subversion(cmd).split(/\n/).delete_if do |line|
|
591
|
+
status = subversion(cmd).delete_if do |line|
|
592
|
+
line =~ /(^X\s+)|(^\s*$)|(^Performing\s)|(^\?\s+README.txt)/
|
593
|
+
end
|
594
|
+
|
595
|
+
if status.length > 0
|
596
|
+
# NOT NEEDED SINCE status COMMAND LISTS
|
597
|
+
# status.each { |line| $watcher.debug(line) }
|
598
|
+
raise RuntimeError.new("Must commit changes to branch first")
|
599
|
+
end
|
600
|
+
|
601
|
+
# execute will throw if branch missing
|
602
|
+
# If here, then branch was found
|
603
|
+
subversion_committed = true
|
604
|
+
rescue
|
605
|
+
# no-op
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
return subversion_committed
|
610
|
+
end
|
611
|
+
|
612
|
+
######################################################################
|
613
|
+
def switch_branch! (repos, branch_dir, branch_path)
|
614
|
+
# branch_dir -- Subversion work directory where switch
|
615
|
+
# branch_path -- Subversion branch path
|
616
|
+
|
617
|
+
msg = "Switching [#{branch_dir}] directory to newly created "
|
618
|
+
msg << "branch [#{branch_path}]"
|
619
|
+
$watcher.verbose(msg) do
|
620
|
+
cmd = "svn switch #{repos}/#{branch_path} #{branch_dir}"
|
621
|
+
subversion(cmd)
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
######################################################################
|
626
|
+
def switch_this_dir! (branch_name)
|
627
|
+
# branch_name -- branch name to switch to
|
628
|
+
|
629
|
+
repos = nil
|
630
|
+
|
631
|
+
# If trunk specified, no 'branches' prefix is required.
|
632
|
+
if branch_name == 'trunk'
|
633
|
+
branch_path = branch_name
|
634
|
+
else
|
635
|
+
branch_path = "branches/#{branch_name}"
|
636
|
+
end
|
637
|
+
$watcher.debug("branch_path = [#{branch_path}]")
|
638
|
+
|
639
|
+
$watcher.verbose("Checking work environment") do
|
640
|
+
current_dir = inspect_current_work_directory
|
641
|
+
repos = current_dir[:repos]
|
642
|
+
$watcher.debug("repository = [#{repos}]")
|
643
|
+
# "https://svn.example.com/svn/project_name"
|
644
|
+
working_branch = current_dir[:branch]
|
645
|
+
# "trunk"
|
646
|
+
# "branches/2.1-maint"
|
647
|
+
|
648
|
+
# Check for Subversion commit in this directory
|
649
|
+
verify_subversion_commit(Dir.getwd())
|
650
|
+
|
651
|
+
# Verify / create branch and sub-branches on repository server
|
652
|
+
verify_branch!(repos, branch_path)
|
653
|
+
end
|
654
|
+
|
655
|
+
switch_branch!(repos, Dir.getwd(), branch_path)
|
656
|
+
end
|
657
|
+
|
658
|
+
######################################################################
|
659
|
+
def tag_branch! (repos, branch, tag)
|
660
|
+
# branch -- branch into which changes are placed
|
661
|
+
# tag -- tag to use to mark the start or end of branch
|
662
|
+
# (for merging)
|
663
|
+
|
664
|
+
status = if tag =~ /-PRE$/
|
665
|
+
'the start of '
|
666
|
+
elsif tag =~ /-POST$/
|
667
|
+
'the end of '
|
668
|
+
else
|
669
|
+
''
|
670
|
+
end
|
671
|
+
|
672
|
+
msg = "Creating Subversion tag to mark #{status}[#{branch}]"
|
673
|
+
$watcher.verbose(msg) do
|
674
|
+
branch = repos + '/' + branch
|
675
|
+
tag = repos + '/' + tag
|
676
|
+
cmd = "svn copy -m \"#{msg}\" #{branch} #{tag}"
|
677
|
+
# NOTE: Not a 'safe' command because modifies server
|
678
|
+
subversion(cmd, false)
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
######################################################################
|
683
|
+
def update_branch! (repos, branch_name)
|
684
|
+
msg = "Updating Subversion work directory from [#{branch_name}]"
|
685
|
+
$watcher.verbose(msg) do
|
686
|
+
subversion("svn up")
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
######################################################################
|
691
|
+
def verify_branch! (repos, branch)
|
692
|
+
# Ensures branch exists on repository, or throws exception
|
693
|
+
|
694
|
+
# This block sets up our condition handling mechanism:
|
695
|
+
# Make 2 passes: if first fails because of missing branch,
|
696
|
+
# attempt to create the branch.
|
697
|
+
# For other failures, re-raise exception.
|
698
|
+
params = {:failure => :error, :tries => 2, :proc => lambda do |e|
|
699
|
+
if e.message =~ /branch not found/
|
700
|
+
make_repository_path!(repos, branch)
|
701
|
+
else
|
702
|
+
# if not a simple "branch not found" error, then punt
|
703
|
+
raise
|
704
|
+
end
|
705
|
+
end
|
706
|
+
}
|
707
|
+
|
708
|
+
# This code does the verification, and invokes condition handling
|
709
|
+
$watcher.verbose("Verifying repository branch [#{branch}]", params) do
|
710
|
+
unless branch_exists?(repos, branch)
|
711
|
+
raise RuntimeError.new("branch not found [#{branch}]")
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
######################################################################
|
717
|
+
def verify_path! (path)
|
718
|
+
# Ensures path exists on local system, or throws exception
|
719
|
+
|
720
|
+
# This block sets up our condition handling mechanism:
|
721
|
+
# Make 2 passes: if first fails because of missing path,
|
722
|
+
# attempt to create the path.
|
723
|
+
# For other failures, re-raise exception.
|
724
|
+
params = {:failure => :error, :tries => 2, :proc => lambda do |e|
|
725
|
+
if e.message =~ /path not found/
|
726
|
+
FileUtils.mkdir_p(path)
|
727
|
+
else
|
728
|
+
# if not a simple "path not found" error, then punt
|
729
|
+
raise
|
730
|
+
end
|
731
|
+
end
|
732
|
+
}
|
733
|
+
|
734
|
+
# This code does the verification, and invokes condition handling
|
735
|
+
$watcher.verbose("Verifying path exists [#{path}]", params) do
|
736
|
+
unless File.directory?(path)
|
737
|
+
raise RuntimeError.new("path not found [#{path}]")
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
######################################################################
|
743
|
+
def verify_subversion_commit (branch_dir)
|
744
|
+
msg = "Verifying data in this Subversion working directory are"
|
745
|
+
msg << " committed [#{branch_dir}]"
|
746
|
+
$watcher.verbose(msg) do
|
747
|
+
unless subversion_committed?(branch_dir)
|
748
|
+
msg = "Must commit changes to working directory first."
|
749
|
+
raise RuntimeError.new(msg)
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
######################################################################
|
755
|
+
# DO NOT ENCLOSE; MUST RUN AT TOP-LEVEL OF FILE FOR BIN INSTALLATION.
|
756
|
+
# if $0 == __FILE__
|
757
|
+
start_dir = Dir.getwd()
|
758
|
+
|
759
|
+
force = false # Do not force a change if error
|
760
|
+
switch = false # Switch present working directory
|
761
|
+
options = true
|
762
|
+
verbosity = :verbose
|
763
|
+
|
764
|
+
# $DEBUG = true
|
765
|
+
|
766
|
+
while options
|
767
|
+
case ARGV[0]
|
768
|
+
when '-d' : verbosity = :debug ; ARGV.shift
|
769
|
+
when '-f' : force = true ; ARGV.shift
|
770
|
+
when '-s', '--switch' : switch = true ; ARGV.shift
|
771
|
+
when '-v' : verbosity = :verbose ; ARGV.shift
|
772
|
+
when '--version'
|
773
|
+
puts Svnbranch::VERSION
|
774
|
+
exit
|
775
|
+
when '-q' : verbosity = :always ; ARGV.shift
|
776
|
+
else options = false
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
$watcher = Watcher.create(:verbosity => verbosity,
|
781
|
+
:warn_symbol => 'WARNING',
|
782
|
+
:error_symbol => 'ERROR')
|
783
|
+
|
784
|
+
begin
|
785
|
+
errors = false
|
786
|
+
$watcher.debug("Validating command line arguments") do
|
787
|
+
msg = "usage:\n"
|
788
|
+
case ARGV.shift
|
789
|
+
when '-h', '--help', 'help'
|
790
|
+
man_page
|
791
|
+
exit
|
792
|
+
when 'create'
|
793
|
+
if ARGV.length == 1
|
794
|
+
create_branch!(ARGV.shift, force, !switch)
|
795
|
+
else
|
796
|
+
msg << " #{File.basename($0)} [ -s ] create [<sub-branch>/]<new-branch>\n\n"
|
797
|
+
STDERR.puts msg
|
798
|
+
raise ArgumentError.new("Invalid command line arguments")
|
799
|
+
end
|
800
|
+
when 'close'
|
801
|
+
if switch == false and ARGV.length == 0
|
802
|
+
close_branch!(force)
|
803
|
+
else
|
804
|
+
msg << " #{File.basename($0)} close\n\n"
|
805
|
+
STDERR.puts msg
|
806
|
+
raise ArgumentError.new("Invalid command line arguments")
|
807
|
+
end
|
808
|
+
when 'merge'
|
809
|
+
if switch == false and ARGV.length == 1
|
810
|
+
merge_branch!(ARGV.shift)
|
811
|
+
else
|
812
|
+
msg << " #{File.basename($0)} merge [<sub-branch>/]<merge-branch>\n\n"
|
813
|
+
STDERR.puts msg
|
814
|
+
raise ArgumentError.new("Invalid command line arguments")
|
815
|
+
end
|
816
|
+
when 'delete'
|
817
|
+
if switch == false and ARGV.length == 1
|
818
|
+
delete_branch!(ARGV.shift)
|
819
|
+
else
|
820
|
+
msg << " #{File.basename($0)} delete [<sub_branch>/]<doomed-branch>\n\n"
|
821
|
+
STDERR.puts msg
|
822
|
+
raise ArgumentError.new("Invalid command line arguments")
|
823
|
+
end
|
824
|
+
when 'switch'
|
825
|
+
if switch == false and ARGV.length == 1
|
826
|
+
switch_this_dir!(ARGV.shift)
|
827
|
+
else
|
828
|
+
msg << " #{File.basename($0)} switch [<sub_branch>/]<switch-branch>\n\n"
|
829
|
+
STDERR.puts msg
|
830
|
+
raise ArgumentError.new("Invalid command line arguments")
|
831
|
+
end
|
832
|
+
else
|
833
|
+
msg << " #{File.basename($0)} [ -s ] create [<sub-branch>/]<new-branch>\n"
|
834
|
+
msg << " #{File.basename($0)} close\n"
|
835
|
+
msg << " #{File.basename($0)} merge [<sub-branch>/]<merge-branch>\n"
|
836
|
+
msg << " #{File.basename($0)} delete [<sub_branch>/]<doomed-branch>\n"
|
837
|
+
msg << " #{File.basename($0)} switch [<sub_branch>/]<switch-branch>\n\n"
|
838
|
+
STDERR.puts msg
|
839
|
+
raise ArgumentError.new("Invalid command line arguments")
|
840
|
+
end
|
841
|
+
end
|
842
|
+
rescue
|
843
|
+
errors = true
|
844
|
+
Dir.chdir(start_dir)
|
845
|
+
end
|
846
|
+
|
847
|
+
exit errors == false
|
848
|
+
#end
|