svn-merge-vendor.py [plain text]
import os
import re
import tempfile
import atexit
import subprocess
import shutil
import sys
import getopt
import logging
import string
from StringIO import StringIO
from lxml import etree
import types
prog_name = os.path.basename(sys.argv[0])
orig_svn_subroot = None
base_copied_paths = []
r_from = None
r_to = None
log_tree = None
entries_to_treat = []
entries_to_delete = []
added_paths = []
logger = None
def del_temp_tree(tmpdir):
"""Delete tree, standring in the root"""
global logger
logger.info("Deleting tmpdir "+tmpdir)
os.chdir("/")
try:
shutil.rmtree(tmpdir)
except OSError:
print logger.warn("Couldn't delete tmpdir %s. Don't forget to remove it manually." % (tmpdir))
def checkout(url, revision=None):
"""Checks out the given URL at the given revision, using HEAD if not defined. Returns the working copy directory"""
global logger
wc_dir = tempfile.mkdtemp(prefix=prog_name)
atexit.register(del_temp_tree, wc_dir)
if (revision):
url += "@"+revision
logger.info("Checking out "+url+" to "+wc_dir)
returncode = call_cmd(["svn", "checkout", url, wc_dir])
if (returncode == 1):
return None
else:
return wc_dir
def merge(wc_dir, revision_from, revision_to):
"""Merges repo_url from revision revision_from to revision revision_to into wc_dir"""
global logger
logger.info("Merging between revisions %s and %s into %s" % (revision_from, revision_to, wc_dir))
os.chdir(wc_dir)
return call_cmd(["svn", "merge", "-r", revision_from+":"+revision_to, wc_dir])
def treat_status(wc_dir_orig, wc_dir):
"""Copies modification from official vendor branch to wc"""
global logger
logger.info("Copying modification from official vendor branch %s to wc %s" % (wc_dir_orig, wc_dir))
os.chdir(wc_dir_orig)
status_tree = call_cmd_xml_tree_out(["svn", "status", "--xml"])
global entries_to_treat, entries_to_delete
entries_to_treat = status_tree.xpath("/status/target/entry")
entries_to_delete = []
while len(entries_to_treat) > 0:
entry = entries_to_treat.pop(0)
entry_type = get_entry_type(entry)
file = get_entry_path(entry)
if entry_type == 'added':
if is_entry_copied(entry):
check_exit(copy(wc_dir_orig, wc_dir, file), "Error during copy")
else:
check_exit(add(wc_dir_orig, wc_dir, file), "Error during add")
elif entry_type == 'deleted':
entries_to_delete.append(entry)
elif entry_type == 'modified' or entry_type == 'replaced':
check_exit(update(wc_dir_orig, wc_dir, file), "Error during update")
elif entry_type == 'normal':
logger.info("File %s has a 'normal' state (unchanged). Ignoring." % (file))
else:
logger.error("Status not understood : '%s' not supported (file : %s)" % (entry_type, file))
for entry in entries_to_delete:
check_exit(delete(wc_dir_orig, wc_dir, get_entry_path(entry)), "Error during delete")
return 0
def get_entry_type(entry):
return get_xml_text_content(entry, "wc-status/@item")
def get_entry_path(entry):
return get_xml_text_content(entry, "@path")
def is_entry_copied(entry):
return get_xml_text_content(entry, "wc-status/@copied") == 'true'
def copy(wc_dir_orig, wc_dir, file):
global logger
logger.info("A+ %s" % (file))
os.chdir(wc_dir_orig)
info_tree = call_cmd_xml_tree_out(["svn", "info", "--xml", os.path.join(wc_dir_orig, file)])
url = get_xml_text_content(info_tree, "/info/entry/url")
global orig_svn_subroot
if not orig_svn_subroot:
orig_svn_root = get_xml_text_content(info_tree, "/info/entry/repository/root")
sub_url = url.split(orig_svn_root)[-1]
sub_url = os.path.normpath(sub_url)
if sub_url.startswith(os.path.sep):
sub_url = sub_url[1:]
orig_svn_subroot = '/'+sub_url.split(file)[0].replace(os.path.sep, '/')
global log_tree
if not log_tree:
os.chdir(wc_dir_orig)
orig_svn_root_subroot = get_xml_text_content(info_tree, "/info/entry/repository/root") + orig_svn_subroot
real_from = str(int(r_from)+1)
logger.info("Retreiving log of the original trunk %s between revisions %s and %s ..." % (orig_svn_root_subroot, real_from, r_to))
log_tree = call_cmd_xml_tree_out(["svn", "log", "--xml", "-v", "-r", "%s:%s" % (real_from, r_to), orig_svn_root_subroot])
orig_url_file = orig_svn_subroot+file.replace(os.path.sep, '/')
orig_url_file_old = None
while orig_url_file:
orig_url_file_old = orig_url_file
orig_url_file = get_xml_text_content(log_tree, "//path[(@action='R' or @action='A') and text()='%s']/@copyfrom-path" % (orig_url_file))
logger.debug("orig_url_file : %s" % (orig_url_file))
orig_url_file = orig_url_file_old
if orig_url_file:
orig_file = convert_relative_url_to_path(orig_url_file)
else:
orig_file = None
global base_copied_paths, added_paths
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)):
for path in base_copied_paths:
if file.startswith(path):
logger.warn("The path %s to add is a sub-path of recently copied %s. Ignoring the A+." % (file, path))
return 0
logger.warn("Log paths for the file %s don't correspond with any file in the wc. Will do a simple A." % (file))
return add(wc_dir_orig, wc_dir, file)
orig_file = convert_relative_url_to_path(orig_url_file)
cmd = 'copy'
global entries_to_treat, entries_to_delete
if search_and_remove_delete_entry(entries_to_treat, orig_file) or search_and_remove_delete_entry(entries_to_delete, orig_file):
cmd = 'move'
logger.info("%s from %s" % (cmd, orig_url_file))
returncode = call_cmd(["svn", cmd, os.path.join(wc_dir, orig_file), os.path.join(wc_dir, file)])
if returncode == 0:
if os.path.isdir(os.path.join(wc_dir, orig_file)):
base_copied_paths.append(file)
else:
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
return returncode
def search_and_remove_delete_entry(entries, orig_file):
for entry in entries:
if get_entry_type(entry) == 'deleted' and get_entry_path(entry) == orig_file:
entries.remove(entry)
return True
return False
def convert_relative_url_to_path(url):
global orig_svn_subroot
return os.path.normpath(url.split(orig_svn_subroot)[-1])
def new_added_path(returncode, file):
if not is_returncode_bad(returncode):
global added_paths
added_paths.append(file)
def add(wc_dir_orig, wc_dir, file):
global logger
logger.info("A %s" % (file))
if os.path.exists(os.path.join(wc_dir, file)):
logger.warn("Target file %s already exists. Will do a simple M" % (file))
return update(wc_dir_orig, wc_dir, file)
os.chdir(wc_dir)
if os.path.isdir(os.path.join(wc_dir_orig, file)):
returncode = call_cmd(["svn", "mkdir", file])
new_added_path(returncode, file)
return returncode
else:
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
returncode = call_cmd(["svn", "add", file])
new_added_path(returncode, file)
return returncode
def delete(wc_dir_orig, wc_dir, file):
global logger
logger.info("D %s" % (file))
os.chdir(wc_dir)
if not os.path.exists(file):
logger.warn("File %s doesn't exist. Ignoring D." % (file))
return 0
return call_cmd(["svn", "delete", file])
def update(wc_dir_orig, wc_dir, file):
global logger
logger.info("M %s" % (file))
if os.path.isdir(os.path.join(wc_dir_orig, file)):
logger.warn("%s is a directory. Ignoring M." % (file))
return 0
shutil.copy(os.path.join(wc_dir_orig, file), os.path.join(wc_dir, file))
return 0
def fine_tune(wc_dir):
"""Gives the user a chance to fine-tune"""
alert(["If you want to fine-tune import, do so in working copy located at : %s" % (wc_dir),
"When done, press Enter to commit, or Ctrl-C to abort."])
def alert(messages):
"""Wait the user to <ENTER> or abort the program"""
for message in messages:
print >> sys.stderr, message
try:
return sys.stdin.readline()
except KeyboardInterrupt:
sys.exit(0)
def commit(wc_dir, message):
"""Commits the wc_dir"""
os.chdir(wc_dir)
cmd = ["svn", "commit"]
if (message):
cmd += ["-m", message]
return call_cmd(cmd)
def tag_wc(repo_url, current, tag, message):
"""Tags the wc_dir"""
cmd = ["svn", "copy"]
if (message):
cmd += ["-m", message]
return call_cmd(cmd + [repo_url+"/"+current, repo_url+"/"+tag])
def call_cmd(cmd):
global logger
logger.debug(string.join(cmd, ' '))
return subprocess.call(cmd, stdout=DEVNULL, stderr=sys.stderr)
def call_cmd_out(cmd):
global logger
logger.debug(string.join(cmd, ' '))
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=sys.stderr).stdout
def call_cmd_str_out(cmd):
out = call_cmd_out(cmd)
str_out = ""
for line in out.readlines():
str_out += line
out.close()
return str_out
def call_cmd_xml_tree_out(cmd):
return etree.parse(StringIO(call_cmd_str_out(cmd)))
def get_xml_text_content(xml_doc, xpath):
result_nodes = xml_doc.xpath(xpath)
if result_nodes:
if type(result_nodes[0]) == types.StringType:
return result_nodes[0]
else:
return result_nodes[0].text
else:
return None
def usage(error = None):
"""Print usage message and exit"""
print >>sys.stderr, """%s: Merges the difference between two revisions of the original repository of the vendor, to the vendor branch
usage: %s [options] REPO_URL CURRENT_PATH ORIGINAL_REPO_URL -r N:M
- REPO_URL : repository URL for the vendor branch (i.e: http://svn.example.com/repos/vendor/libcomplex)
- CURRENT_PATH : relative path to the current folder (i.e: current)
- ORIGINAL_REPO_URL : original base repository URL
- N:M : from revision N to revision M
This command executes these steps:
1. Check out directory specified by ORIGINAL_REPO_URL@N in a temporary directory.(1)
2. Merges changes to revision M.(1)
3. Check out directory specified by REPO_URL in a second temporary directory.(2)
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.
5. Allow user to fine-tune import.
6. Commit.
7. Optionally tag new release.
8. Delete the temporary directories.
(1) : if -c wasn't passed
(2) : if -w wasn't passed
Valid options:
-r [--revision] N:M : specify revisions N to M
-h [--help] : show this usage
-t [--tag] arg : copy new release to directory ARG, relative to REPO_URL,
using automatic commit message. Example:
-t ../0.42
--non-interactive : do no interactive prompting, do not allow manual fine-tune
-m [--message] arg : specify commit message ARG
-v [--verbose] : verbose mode
-c [--merged-vendor] arg : working copy path of the original already merged vendor trunk (skips the steps 1. and 2.)
-w [--current-wc] arg : working copy path of the current checked out trunk of the vendor branch (skips the step 3.)
""" % ((prog_name,) * 2)
if error:
print >>sys.stder, "", "Current error : "+error
sys.exit(1)
def main():
tag = None
message = None
interactive = 1
revision_to_parse = None
merged_vendor = None
wc_dir = None
global logger
logger = logging.getLogger('svn-merge-vendor')
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(levelname)-8s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.INFO)
try:
opts, args = getopt.gnu_getopt(sys.argv[1:], "ht:m:vr:c:w:",
["help", "tag", "message", "non-interactive", "verbose", "revision", "merged-vendor", "current-wc"])
except getopt.GetoptError:
usage()
for o, a in opts:
if o in ("-h", "--help"):
usage()
if o in ("-t", "--tag"):
tag = a
if o in ("-m", "--message"):
message = a
if o in ("--non-interactive"):
interactive = 0
if o in ("-v", "--verbose"):
logger.setLevel(logging.DEBUG)
if o in ("-r", "--revision"):
revision_to_parse = a
if o in ("-c", "--merged-vendor"):
merged_vendor = a
if o in ("-w", "--current-wc"):
wc_dir = a
if len(args) != 3:
usage()
repo_url, current_path, orig_repo_url = args[0:3]
if (not revision_to_parse):
usage("the revision numbers are mendatory")
global r_from, r_to
r_from, r_to = re.match("(\d+):(\d+)", revision_to_parse).groups()
if not r_from or not r_to:
usage("the revision numbers are mendatory")
try:
r_from_int = int(r_from)
r_to_int = int(r_to)
except ValueError:
usage("the revision parameter is not a number")
if r_from_int >= r_to_int:
usage("the 'from revision' must be inferior to the 'to revision'")
if not merged_vendor:
if orig_repo_url.startswith("http://"):
wc_dir_orig = checkout(orig_repo_url, r_from)
check_exit(wc_dir_orig, "Error during checkout")
check_exit(merge(wc_dir_orig, r_from, r_to), "Error during merge")
else:
usage("ORIGINAL_REPO_URL must start with 'http://'")
else:
wc_dir_orig = merged_vendor
if not wc_dir:
wc_dir = checkout(repo_url+"/"+current_path)
check_exit(wc_dir, "Error during checkout")
check_exit(treat_status(wc_dir_orig, wc_dir), "Error during resolving")
if (interactive):
fine_tune(wc_dir)
if not message:
message = "New vendor version, upgrading from revision %s to revision %s" % (r_from, r_to)
alert(["No message was specified to commit, the program will use that default one : '%s'" % (message),
"Press Enter to commit, or Ctrl-C to abort."])
check_exit(commit(wc_dir, message), "Error during commit")
if tag:
if not message:
message = "Tag %s, when upgrading the vendor branch from revision %s to revision %s" % (tag, r_from, r_to)
alert(["No message was specified to tag, the program will use that default one : '%s'" % (message),
"Press Enter to tag, or Ctrl-C to abort."])
check_exit(tag_wc(repo_url, current_path, tag, message), "Error during tag")
logger.info("Vendor branch merged, passed from %s to %s !" % (r_from, r_to))
def is_returncode_bad(returncode):
return returncode is None or returncode == 1
def check_exit(returncode, message):
global logger
if is_returncode_bad(returncode):
logger.error(message)
sys.exit(1)
if __name__ == "__main__":
if (os.name == "nt"):
DEVNULL = open("nul:", "w")
else:
DEVNULL = open("/dev/null", "w")
main()