2
# -*-mode: python; coding: utf-8 -*-
4
# Inspired from svn-import.py by astrand@cendio.se (ref :
5
# http://svn.haxx.se/users/archive-2006-10/0857.shtml)
7
# svn-merge-vendor.py (v1.0.1) - Import a new release, such as a vendor drop.
9
# The "Vendor branches" chapter of "Version Control with Subversion"
10
# describes how to do a new vendor drop with:
12
# >The goal here is to make our current directory contain only the
13
# >libcomplex 1.1 code, and to ensure that all that code is under version
14
# >control. Oh, and we want to do this with as little version control
15
# >history disturbance as possible.
17
# This utility tries to take you to this goal - automatically. Files
18
# new in this release is added to version control, and files removed
19
# in this new release are removed from version control. It will
20
# detect the moved files by looking in the svn log to find the
21
# "copied-from" path !
23
# Compared to svn_load_dirs.pl, this utility:
25
# * DETECTS THE MOVED FILES !!
26
# * Does not hard-code commit messages
27
# * Allows you to fine-tune the import before commit, which
28
# allows you to turn adds+deletes into moves.
31
# * support --username and --password
33
# This tool is provided under GPL license. Please read
34
# http://www.gnu.org/licenses/gpl.html for the original text.
36
# $HeadURL: http://svn.apache.org/repos/asf/subversion/branches/1.6.x/contrib/client-side/svn-merge-vendor.py $
37
# $LastChangedRevision: 867363 $
38
# $LastChangedDate: 2007-10-19 08:17:06 +0000 (Fri, 19 Oct 2007) $
39
# $LastChangedBy: aogier $
51
from StringIO import StringIO
52
# lxml module can be found here : http://codespeak.net/lxml/
53
from lxml import etree
56
prog_name = os.path.basename(sys.argv[0])
57
orig_svn_subroot = None
58
base_copied_paths = []
63
entries_to_delete = []
67
def del_temp_tree(tmpdir):
68
"""Delete tree, standring in the root"""
70
logger.info("Deleting tmpdir "+tmpdir)
75
print logger.warn("Couldn't delete tmpdir %s. Don't forget to remove it manually." % (tmpdir))
78
def checkout(url, revision=None):
79
"""Checks out the given URL at the given revision, using HEAD if not defined. Returns the working copy directory"""
81
# Create a temp dir to hold our working copy
82
wc_dir = tempfile.mkdtemp(prefix=prog_name)
83
atexit.register(del_temp_tree, wc_dir)
89
logger.info("Checking out "+url+" to "+wc_dir)
90
returncode = call_cmd(["svn", "checkout", url, wc_dir])
97
def merge(wc_dir, revision_from, revision_to):
98
"""Merges repo_url from revision revision_from to revision revision_to into wc_dir"""
100
logger.info("Merging between revisions %s and %s into %s" % (revision_from, revision_to, wc_dir))
102
return call_cmd(["svn", "merge", "-r", revision_from+":"+revision_to, wc_dir])
104
def treat_status(wc_dir_orig, wc_dir):
105
"""Copies modification from official vendor branch to wc"""
107
logger.info("Copying modification from official vendor branch %s to wc %s" % (wc_dir_orig, wc_dir))
108
os.chdir(wc_dir_orig)
109
status_tree = call_cmd_xml_tree_out(["svn", "status", "--xml"])
110
global entries_to_treat, entries_to_delete
111
entries_to_treat = status_tree.xpath("/status/target/entry")
112
entries_to_delete = []
114
while len(entries_to_treat) > 0:
115
entry = entries_to_treat.pop(0)
116
entry_type = get_entry_type(entry)
117
file = get_entry_path(entry)
118
if entry_type == 'added':
119
if is_entry_copied(entry):
120
check_exit(copy(wc_dir_orig, wc_dir, file), "Error during copy")
122
check_exit(add(wc_dir_orig, wc_dir, file), "Error during add")
123
elif entry_type == 'deleted':
124
entries_to_delete.append(entry)
125
elif entry_type == 'modified' or entry_type == 'replaced':
126
check_exit(update(wc_dir_orig, wc_dir, file), "Error during update")
127
elif entry_type == 'normal':
128
logger.info("File %s has a 'normal' state (unchanged). Ignoring." % (file))
130
logger.error("Status not understood : '%s' not supported (file : %s)" % (entry_type, file))
132
# We then treat the left deletions
133
for entry in entries_to_delete:
134
check_exit(delete(wc_dir_orig, wc_dir, get_entry_path(entry)), "Error during delete")
138
def get_entry_type(entry):
139
return get_xml_text_content(entry, "wc-status/@item")
141
def get_entry_path(entry):
142
return get_xml_text_content(entry, "@path")
144
def is_entry_copied(entry):
145
return get_xml_text_content(entry, "wc-status/@copied") == 'true'
147
def copy(wc_dir_orig, wc_dir, file):
149
logger.info("A+ %s" % (file))
151
# Retreiving the original URL
152
os.chdir(wc_dir_orig)
153
info_tree = call_cmd_xml_tree_out(["svn", "info", "--xml", os.path.join(wc_dir_orig, file)])
154
url = get_xml_text_content(info_tree, "/info/entry/url")
156
# Detecting original svn root
157
global orig_svn_subroot
158
if not orig_svn_subroot:
159
orig_svn_root = get_xml_text_content(info_tree, "/info/entry/repository/root")
160
#print >>sys.stderr, "url : %s" % (url)
161
sub_url = url.split(orig_svn_root)[-1]
162
sub_url = os.path.normpath(sub_url)
163
#print >>sys.stderr, "sub_url : %s" % (sub_url)
164
if sub_url.startswith(os.path.sep):
165
sub_url = sub_url[1:]
167
orig_svn_subroot = '/'+sub_url.split(file)[0].replace(os.path.sep, '/')
168
#print >>sys.stderr, "orig_svn_subroot : %s" % (orig_svn_subroot)
172
# Detecting original file copy path
173
os.chdir(wc_dir_orig)
174
orig_svn_root_subroot = get_xml_text_content(info_tree, "/info/entry/repository/root") + orig_svn_subroot
175
real_from = str(int(r_from)+1)
176
logger.info("Retreiving log of the original trunk %s between revisions %s and %s ..." % (orig_svn_root_subroot, real_from, r_to))
177
log_tree = call_cmd_xml_tree_out(["svn", "log", "--xml", "-v", "-r", "%s:%s" % (real_from, r_to), orig_svn_root_subroot])
179
# Detecting the path of the original moved or copied file
180
orig_url_file = orig_svn_subroot+file.replace(os.path.sep, '/')
181
orig_url_file_old = None
182
#print >>sys.stderr, " orig_url_file : %s" % (orig_url_file)
184
orig_url_file_old = orig_url_file
185
orig_url_file = get_xml_text_content(log_tree, "//path[(@action='R' or @action='A') and text()='%s']/@copyfrom-path" % (orig_url_file))
186
logger.debug("orig_url_file : %s" % (orig_url_file))
187
orig_url_file = orig_url_file_old
189
# Getting the relative url for the original url file
191
orig_file = convert_relative_url_to_path(orig_url_file)
194
global base_copied_paths, added_paths
195
# If there is no "moved origin" for that file, or the origin doesn't exist in the working directory, or the origin is the same as the given file, or the origin is an added file
196
if not orig_url_file or (orig_file and (not os.path.exists(os.path.join(wc_dir, orig_file)) or orig_file == file or orig_file in added_paths)):
197
# Check if the file is within a recently copied path
198
for path in base_copied_paths:
199
if file.startswith(path):
200
logger.warn("The path %s to add is a sub-path of recently copied %s. Ignoring the A+." % (file, path))
202
# Simple add the file
203
logger.warn("Log paths for the file %s don't correspond with any file in the wc. Will do a simple A." % (file))
204
return add(wc_dir_orig, wc_dir, file)
206
# We catch the relative URL for the original file
207
orig_file = convert_relative_url_to_path(orig_url_file)
209
# Detect if it's a move
211
global entries_to_treat, entries_to_delete
212
if search_and_remove_delete_entry(entries_to_treat, orig_file) or search_and_remove_delete_entry(entries_to_delete, orig_file):
213
# It's a move, removing the delete, and treating it as a move
216
logger.info("%s from %s" % (cmd, orig_url_file))
217
returncode = call_cmd(["svn", cmd, os.path.join(wc_dir, orig_file), os.path.join(wc_dir, file)])
219
if os.path.isdir(os.path.join(wc_dir, orig_file)):
220
base_copied_paths.append(file)
222
# Copy the last version of the file from the original repository
223
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
226
def search_and_remove_delete_entry(entries, orig_file):
227
for entry in entries:
228
if get_entry_type(entry) == 'deleted' and get_entry_path(entry) == orig_file:
229
entries.remove(entry)
233
def convert_relative_url_to_path(url):
234
global orig_svn_subroot
235
return os.path.normpath(url.split(orig_svn_subroot)[-1])
237
def new_added_path(returncode, file):
238
if not is_returncode_bad(returncode):
240
added_paths.append(file)
242
def add(wc_dir_orig, wc_dir, file):
244
logger.info("A %s" % (file))
245
if os.path.exists(os.path.join(wc_dir, file)):
246
logger.warn("Target file %s already exists. Will do a simple M" % (file))
247
return update(wc_dir_orig, wc_dir, file)
249
if os.path.isdir(os.path.join(wc_dir_orig, file)):
250
returncode = call_cmd(["svn", "mkdir", file])
251
new_added_path(returncode, file)
254
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
255
returncode = call_cmd(["svn", "add", file])
256
new_added_path(returncode, file)
259
def delete(wc_dir_orig, wc_dir, file):
261
logger.info("D %s" % (file))
263
if not os.path.exists(file):
264
logger.warn("File %s doesn't exist. Ignoring D." % (file))
266
return call_cmd(["svn", "delete", file])
268
def update(wc_dir_orig, wc_dir, file):
270
logger.info("M %s" % (file))
271
if os.path.isdir(os.path.join(wc_dir_orig, file)):
272
logger.warn("%s is a directory. Ignoring M." % (file))
274
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
277
def fine_tune(wc_dir):
278
"""Gives the user a chance to fine-tune"""
279
alert(["If you want to fine-tune import, do so in working copy located at : %s" % (wc_dir),
280
"When done, press Enter to commit, or Ctrl-C to abort."])
283
"""Wait the user to <ENTER> or abort the program"""
284
for message in messages:
285
print >> sys.stderr, message
287
return sys.stdin.readline()
288
except KeyboardInterrupt:
291
def commit(wc_dir, message):
292
"""Commits the wc_dir"""
294
cmd = ["svn", "commit"]
296
cmd += ["-m", message]
299
def tag_wc(repo_url, current, tag, message):
300
"""Tags the wc_dir"""
301
cmd = ["svn", "copy"]
303
cmd += ["-m", message]
304
return call_cmd(cmd + [repo_url+"/"+current, repo_url+"/"+tag])
308
logger.debug(string.join(cmd, ' '))
309
return subprocess.call(cmd, stdout=DEVNULL, stderr=sys.stderr)#subprocess.STDOUT)
311
def call_cmd_out(cmd):
313
logger.debug(string.join(cmd, ' '))
314
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=sys.stderr).stdout
316
def call_cmd_str_out(cmd):
317
out = call_cmd_out(cmd)
319
for line in out.readlines():
324
def call_cmd_xml_tree_out(cmd):
325
return etree.parse(StringIO(call_cmd_str_out(cmd)))
327
def get_xml_text_content(xml_doc, xpath):
328
result_nodes = xml_doc.xpath(xpath)
330
if type(result_nodes[0]) == types.StringType:
331
return result_nodes[0]
333
return result_nodes[0].text
337
def usage(error = None):
338
"""Print usage message and exit"""
339
print >>sys.stderr, """%s: Merges the difference between two revisions of the original repository of the vendor, to the vendor branch
340
usage: %s [options] REPO_URL CURRENT_PATH ORIGINAL_REPO_URL -r N:M
342
- REPO_URL : repository URL for the vendor branch (i.e: http://svn.example.com/repos/vendor/libcomplex)
343
- CURRENT_PATH : relative path to the current folder (i.e: current)
344
- ORIGINAL_REPO_URL : original base repository URL
345
- N:M : from revision N to revision M
347
This command executes these steps:
349
1. Check out directory specified by ORIGINAL_REPO_URL@N in a temporary directory.(1)
350
2. Merges changes to revision M.(1)
351
3. Check out directory specified by REPO_URL in a second temporary directory.(2)
352
4. Treat the merge by "svn status" on the working copy of ORIGINAL_REPO_URL. If the history is kept ('+' when svn st), do a move instead of a delete / add.
353
5. Allow user to fine-tune import.
355
7. Optionally tag new release.
356
8. Delete the temporary directories.
358
(1) : if -c wasn't passed
359
(2) : if -w wasn't passed
362
-r [--revision] N:M : specify revisions N to M
363
-h [--help] : show this usage
364
-t [--tag] arg : copy new release to directory ARG, relative to REPO_URL,
365
using automatic commit message. Example:
367
--non-interactive : do no interactive prompting, do not allow manual fine-tune
368
-m [--message] arg : specify commit message ARG
369
-v [--verbose] : verbose mode
370
-c [--merged-vendor] arg : working copy path of the original already merged vendor trunk (skips the steps 1. and 2.)
371
-w [--current-wc] arg : working copy path of the current checked out trunk of the vendor branch (skips the step 3.)
372
""" % ((prog_name,) * 2)
375
print >>sys.stder, "", "Current error : "+error
383
revision_to_parse = None
387
# Initializing logger
389
logger = logging.getLogger('svn-merge-vendor')
390
hdlr = logging.StreamHandler(sys.stderr)
391
formatter = logging.Formatter('%(levelname)-8s %(message)s')
392
hdlr.setFormatter(formatter)
393
logger.addHandler(hdlr)
394
logger.setLevel(logging.INFO)
397
opts, args = getopt.gnu_getopt(sys.argv[1:], "ht:m:vr:c:w:",
398
["help", "tag", "message", "non-interactive", "verbose", "revision", "merged-vendor", "current-wc"])
399
except getopt.GetoptError:
400
# print help information and exit:
404
if o in ("-h", "--help"):
406
if o in ("-t", "--tag"):
408
if o in ("-m", "--message"):
410
if o in ("--non-interactive"):
412
if o in ("-v", "--verbose"):
413
logger.setLevel(logging.DEBUG)
414
if o in ("-r", "--revision"):
415
revision_to_parse = a
416
if o in ("-c", "--merged-vendor"):
418
if o in ("-w", "--current-wc"):
424
repo_url, current_path, orig_repo_url = args[0:3]
426
if (not revision_to_parse):
427
usage("the revision numbers are mendatory")
429
r_from, r_to = re.match("(\d+):(\d+)", revision_to_parse).groups()
431
if not r_from or not r_to:
432
usage("the revision numbers are mendatory")
435
r_from_int = int(r_from)
438
usage("the revision parameter is not a number")
440
if r_from_int >= r_to_int:
441
usage("the 'from revision' must be inferior to the 'to revision'")
443
if not merged_vendor:
444
if orig_repo_url.startswith("http://"):
445
wc_dir_orig = checkout(orig_repo_url, r_from)
446
check_exit(wc_dir_orig, "Error during checkout")
448
check_exit(merge(wc_dir_orig, r_from, r_to), "Error during merge")
450
usage("ORIGINAL_REPO_URL must start with 'http://'")
452
wc_dir_orig = merged_vendor
455
wc_dir = checkout(repo_url+"/"+current_path)
456
check_exit(wc_dir, "Error during checkout")
458
check_exit(treat_status(wc_dir_orig, wc_dir), "Error during resolving")
464
message = "New vendor version, upgrading from revision %s to revision %s" % (r_from, r_to)
465
alert(["No message was specified to commit, the program will use that default one : '%s'" % (message),
466
"Press Enter to commit, or Ctrl-C to abort."])
468
check_exit(commit(wc_dir, message), "Error during commit")
472
message = "Tag %s, when upgrading the vendor branch from revision %s to revision %s" % (tag, r_from, r_to)
473
alert(["No message was specified to tag, the program will use that default one : '%s'" % (message),
474
"Press Enter to tag, or Ctrl-C to abort."])
475
check_exit(tag_wc(repo_url, current_path, tag, message), "Error during tag")
477
logger.info("Vendor branch merged, passed from %s to %s !" % (r_from, r_to))
479
def is_returncode_bad(returncode):
480
return returncode is None or returncode == 1
482
def check_exit(returncode, message):
484
if is_returncode_bad(returncode):
485
logger.error(message)
488
if __name__ == "__main__":
489
if (os.name == "nt"):
490
DEVNULL = open("nul:", "w")
492
DEVNULL = open("/dev/null", "w")