def test_switch_dirty_repo(self): common.run_hg(["branch", "test"]) f = open("bar", "w") f.write("bar") f.close() common.run_hg(["add", "bar"]) eq_(False, common.hg_switch_branch("test", "default"))
def map_svn_rev_to_hg(svn_rev, hg_rev="tip", local=False): """ Record the mapping from an SVN revision number and an hg revision (default "tip"). """ args = ["tag"] if local: args.append("-l") args.extend(["-r", strip_hg_rev(hg_rev), "svn.%d" % svn_rev]) run_hg(args)
def get_hg_changes(rev_string): """ Get paths of changed files from a previous revision. Returns a tuple of (added files, removed files, modified files, copied files) Copied files are dict of (dest=>src), others are lists. """ args = ["st", "-armC", "--rev", rev_string] out = run_hg(args, output_is_locale_encoding=True) added = [] removed = [] modified = [] copied = {} skipnext = False for line in out.splitlines(): st = line[0] path = line[2:] if st == 'A': added.append(path) lastadded=path elif st == 'R': removed.append(path) elif st == 'M': modified.append(path) elif st == ' ': added.remove(lastadded) copied[lastadded] = path #print "added:", added #print "removed:", removed #print "modified:", modified return added, removed, modified, copied
def get_hg_changes(rev_string): """ Get paths of changed files from a previous revision. Returns a tuple of (added files, removed files, modified files, copied files) Copied files are dict of (dest=>src), others are lists. """ args = ["st", "-armC", "--rev", rev_string] out = run_hg(args, output_is_locale_encoding=True) added = [] removed = [] modified = [] copied = {} skipnext = False for line in out.splitlines(): st = line[0] path = line[2:] if st == 'A': added.append(path) lastadded = path elif st == 'R': removed.append(path) elif st == 'M': modified.append(path) elif st == ' ': added.remove(lastadded) copied[lastadded] = path #print "added:", added #print "removed:", removed #print "modified:", modified return added, removed, modified, copied
def get_hg_log(start_rev, end_rev): """Get log messages for given changeset range.""" log_args = [ "log", "--verbose", "--follow", "--rev", start_rev + ":" + end_rev, "--prune", start_rev ] return run_hg(log_args)
def get_hg_revs(first_rev, svn_branch, last_rev="tip"): """ Get a chronological list of revisions (changeset IDs) between the two revisions (included). """ args = ["log", "--template", r'{rev}:{node|short}\n', "-b", svn_branch, '--limit', '1000', '--follow', "-r", "%s:%s" % (strip_hg_rev(first_rev), strip_hg_rev(last_rev))] out = run_hg(args) return [strip_hg_rev(s) for s in out.splitlines()]
def get_hg_revs(first_rev, svn_branch, last_rev="tip"): """ Get a chronological list of revisions (changeset IDs) between the two revisions (included). """ args = [ "log", "--template", r'{rev}:{node|short}\n', "-b", svn_branch, '--limit', '1000', '--follow', "-r", "%s:%s" % (strip_hg_rev(first_rev), strip_hg_rev(last_rev)) ] out = run_hg(args) return [strip_hg_rev(s) for s in out.splitlines()]
def test_switch_clean_repo(self): common.run_hg(["branch", "test"]) f = open("bar", "w") f.write("bar") f.close() common.run_hg(["add", "bar"]) common.run_hg(["commit", "-m", '"bar added."']) eq_(True, common.hg_switch_branch("test", "default"))
def setUp(self): self._wd = tempfile.mkdtemp() self._cwd = os.getcwd() os.chdir(self._wd) common.run_hg(["init"]) f = open("foo", "w") f.write("foo") f.close() common.run_hg(["add", "foo"]) common.run_hg(["commit", "-m", "Initial"])
def test_hg(self): s = common.run_hg(['version', '-q']) s = s.split()[0] eq_(s.lower(), 'mercurial')
raise hg_commit_from_svn_log_entry(log_entry) elif unrelated_paths: detect_overwritten_svn_branch(wc_url, svn_rev) # NOTE: in Python 2.5, KeyboardInterrupt isn't a subclass of Exception anymore except (Exception, KeyboardInterrupt), e: ui.status("\nInterrupted, please wait for cleanup!\n", level=ui.ERROR) # NOTE: at this point, running SVN sometimes gives "write lock failures" # which leave the WC in a weird state. time.sleep(0.3) run_svn(["cleanup"]) hgsvn_rev = get_svn_rev_from_hg() if hgsvn_rev != svn_rev: # Unless the tag is there, revert to the last stable state run_svn(["up", "--ignore-externals", "-r", current_rev, svn_wc]) run_hg(["revert", "--all"]) raise return svn_rev def real_main(options, args): if check_for_applied_patches(): print ("There are applied mq patches. Put them aside before " "running hgpullsvn.") return 1 svn_wc = "." # Get SVN info svn_info = get_svn_info('.') current_rev = svn_info['revision']
def real_main(options, args): if run_hg(["st", "-m"]): print ("There are uncommitted changes. Either commit them or put " "them aside before running hgpushsvn.") return 1 if check_for_applied_patches(): print ("There are applied mq patches. Put them aside before " "running hgpushsvn.") return 1 svn_info = get_svn_info(".") svn_current_rev = svn_info["last_changed_rev"] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted' repos_url = svn_info["repos_url"] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2' wc_url = svn_info["url"] assert wc_url.startswith(repos_url) # e.g. u'/branches/xmpp-subprotocols-2178-2' wc_base = wc_url[len(repos_url):] svn_branch = wc_url.split("/")[-1] # Get remote SVN info svn_greatest_rev = get_svn_info(wc_url)['last_changed_rev'] if svn_greatest_rev != svn_current_rev: # We can't go on if the pristine branch isn't up to date. # If the pristine branch lacks some revisions from SVN we are not # able to pull them afterwards. # For example, if the last SVN revision in out hgsvn repository is # r100 and the latest SVN revision is r101, hgpushsvn would create # a tag svn.102 on top of svn.100, but svn.101 isn't in hg history. print ("Branch '%s' out of date. Run 'hgpullsvn' first." % svn_branch) return 1 # Switch branches if necessary. orig_branch = run_hg(["branch"]).strip() if orig_branch != svn_branch: if not hg_switch_branch(orig_branch, svn_branch): return 1 hg_start_rev = "svn.%d" % svn_current_rev hg_revs = None try: hg_start_cset = get_hg_cset(hg_start_rev) except RuntimeError: if not options.force: raise hg_start_cset = get_hg_cset("0") print "Warning: revision '%s' not found, forcing to first rev '%s'" % ( hg_start_rev, hg_start_cset) else: if not options.collapse: hg_revs = get_hg_revs(hg_start_cset, svn_branch) if hg_revs is None: hg_revs = [strip_hg_rev(hg_start_cset), strip_hg_rev(get_hg_cset("tip"))] pushed_svn_revs = [] try: if options.dryrun: print "Outgoing revisions that would be pushed to SVN:" try: for prev_rev, next_rev in get_pairs(hg_revs): if not options.dryrun: if not options.edit: ui.status("Committing changes up to revision %s", get_hg_cset(next_rev)) username = options.username if options.keep_author: username = run_hg(["log", "-r", next_rev, "--template", "{author}"]) svn_rev = hg_push_svn(prev_rev, next_rev, edit=options.edit, username=username, password=options.password, cache=options.cache) if svn_rev: # Issue 95 - added update to prevent add/modify/delete crash run_svn(["up"]) map_svn_rev_to_hg(svn_rev, next_rev, local=True) pushed_svn_revs.append(svn_rev) else: print run_hg(["log", "-r", next_rev, "--template", "{rev}:{node|short} {desc}"]) except: # TODO: Add --no-backup to leave a "clean" repo behind if something # fails? run_hg(["revert", "--all"]) raise finally: work_branch = orig_branch or svn_branch if work_branch != svn_branch: run_hg(["up", "-C", work_branch]) run_hg(["branch", work_branch]) if pushed_svn_revs: if len(pushed_svn_revs) == 1: msg = "Pushed one revision to SVN: " else: msg = "Pushed %d revisions to SVN: " % len(pushed_svn_revs) run_svn(["up", "-r", pushed_svn_revs[-1]]) ui.status("%s %s", msg, ", ".join(str(x) for x in pushed_svn_revs)) for line in run_hg(["st"]).splitlines(): if line.startswith("M"): ui.status(("Mercurial repository has local changes after " "SVN update.")) ui.status(("This may happen with SVN keyword expansions.")) break elif not options.dryrun: ui.status("Nothing to do.")
def main(): usage = """1. %prog [-r SVN rev] [-p SVN peg rev] <SVN URL> [local checkout] 2. %prog --local-only <local checkout>""" parser = OptionParser(usage) parser.add_option("-r", "--svn-rev", type="int", dest="svn_rev", help="SVN revision to checkout from") parser.add_option("-p", "--svn-peg", type="int", dest="svn_peg", help="SVN peg revision to locate checkout URL") parser.add_option("--no-hgignore", dest="hgignore", action="store_false", default=True, help="Don't autogenerate .hgignore") parser.add_option("--local-only", action="store_true", default=False, help="Don't use the server, just build from a local checkout") parser.add_option("--branch", type="string", dest="svn_branch", help="Override branch name (defaults to last path component of <SVN URL>)") (options, args) = run_parser(parser, __doc__) if not 1 <= len(args) <= 2: display_parser_error(parser, "incorrect number of arguments") target_svn_url = args.pop(0).rstrip("/") local_repo = args and args.pop(0) or None if options.svn_peg: target_svn_url += "@" + str(options.svn_peg) if options.local_only: if options.svn_peg: display_parser_error(parser, "--local-only and --svn-peg are mutually exclusive") if options.svn_rev: display_parser_error(parser, "--local-only and --svn-rev are mutually exclusive") if local_repo: display_parser_error(parser, "--local-only does not accept a target directory") # Get SVN info svn_info = get_svn_info(target_svn_url, options.svn_rev) # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted' repos_url = svn_info['repos_url'] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2' actual_svn_url = svn_info['url'] assert actual_svn_url.startswith(repos_url) # if we are allowed to go to the server, lets use it if options.local_only: target_svn_url = os.path.abspath(target_svn_url) local_repo = target_svn_url # e.g. u'/branches/xmpp-subprotocols-2178-2' svn_path = actual_svn_url[len(repos_url):] # e.g. 'xmpp-subprotocols-2178-2' svn_branch = actual_svn_url.split("/")[-1] # if --branch was passed, override the branch name derived above if options.svn_branch: svn_branch = options.svn_branch svn_greatest_rev = svn_info['last_changed_rev'] if options.svn_peg: actual_svn_url += "@" + str(options.svn_peg) if not local_repo: local_repo = actual_svn_url.split("/")[-1] if os.path.exists(local_repo): if not os.path.isdir(local_repo): raise ValueError("%s is not a directory" % local_repo) is_svn_dir = os.path.isdir(os.path.join(local_repo, '.svn')) if is_svn_dir and not options.local_only: nlocal_repo = os.path.abspath(local_repo) print "%s is already a SVN checkout." % nlocal_repo sys.exit(1) elif not is_svn_dir and options.local_only: nlocal_repo = os.path.abspath(local_repo) print "%s must be a SVN checkout for --local-only" % nlocal_repo sys.exit(1) elif options.local_only and is_svn_dir: status = get_svn_status(local_repo, quiet=True) if status: print ("There are uncommitted changes. Either commit them " "or put them aside before running hgimportsvn.") sys.exit(1) else: os.mkdir(local_repo) fixup_hgsvn_dir(local_repo) os.chdir(local_repo) # Get log entry for the SVN revision we will check out svn_copyfrom_path = None svn_copyfrom_revision = None if options.local_only or svn_greatest_rev == 0: # fake up a log message for the initial commit svn_start_log = {} svn_start_log['message'] = 'initial import by hgimportsvn' svn_start_log['revision'] = svn_info['last_changed_rev'] svn_start_log['author'] = svn_info.get('last_changed_author') svn_start_log['date'] = svn_info['last_changed_date'] elif options.svn_rev: # If a specific rev was requested, get log entry just before or at rev svn_start_log = get_last_svn_log_entry(actual_svn_url, 1, options.svn_rev) else: # Otherwise, get log entry of branch creation svn_start_log = get_first_svn_log_entry(actual_svn_url, 1, svn_greatest_rev) for p in svn_start_log['changed_paths']: if p['path'] == svn_path: svn_copyfrom_path = p['copyfrom_path'] svn_copyfrom_revision = p['copyfrom_revision'] break if svn_copyfrom_path: print "SVN branch was copied from '%s' at rev %s" % ( svn_copyfrom_path, svn_copyfrom_revision) else: print "SVN branch isn't a copy" # This is the revision we will checkout from svn_rev = svn_start_log['revision'] # Initialize hg repo if not os.path.exists(".hg"): run_hg(["init"]) if svn_copyfrom_path: # Try to find an hg repo tracking the SVN branch which was copied copyfrom_branch = svn_copyfrom_path.split("/")[-1] hg_copyfrom = os.path.join("..", copyfrom_branch) if (os.path.exists(os.path.join(hg_copyfrom, ".hg")) and os.path.exists(os.path.join(hg_copyfrom, svn_private_dir))): u = get_svn_info(hg_copyfrom)['url'] if u != repos_url + svn_copyfrom_path: print "SVN URL %s in working copy %s doesn't match, ignoring" % (u, hg_copyfrom) else: # Find closest hg tag before requested SVN rev best_tag = None for line in run_hg(["tags", "-R", hg_copyfrom]).splitlines(): if not line.startswith("svn."): continue tag = line.split(None, 1)[0] tag_num = int(tag.split(".")[1]) if tag_num <= svn_copyfrom_revision and (not best_tag or best_tag < tag_num): best_tag = tag_num if not best_tag: print "No hg tag matching rev %s in %s" % (svn_rev, hg_copyfrom) else: # Don't use "pull -u" because it fails with hg 0.9.5 # ("branch default not found") run_hg(["pull", "-r", "svn.%d" % best_tag, hg_copyfrom]) # Not specifying "tip" fails with hg 0.9.5 # ("branch default not found") run_hg(["up", "tip"]) run_hg(["branch", svn_branch]) # Stay on the same filesystem so as to have fast moves if options.local_only: checkout_dir = target_svn_url else: checkout_dir = tempfile.mkdtemp(dir=".") try: # Get SVN manifest and checkout if not options.local_only: svn_checkout(target_svn_url, checkout_dir, svn_rev) svn_manifest = get_svn_versioned_files(checkout_dir) svn_files = set(skip_dirs(svn_manifest, checkout_dir)) svn_dirs = sorted(set(svn_manifest) - svn_files) svn_files = list(svn_files) # in the local case the files here already if not options.local_only: # All directories must exist, including empty ones # (both for hg and for moving .svn dirs later) for d in svn_dirs: if not os.path.isdir(d): if os.path.exists(d): os.remove(d) os.mkdir(d) # Replace checked out files for f in svn_files: if os.path.exists(f): os.remove(f) os.rename(os.path.join(checkout_dir, f), f) try: # Add/remove new/old files if svn_files: run_hg(["addremove"] + hg_exclude_options, svn_files) hg_commit_from_svn_log_entry(svn_start_log) #hg_commit_from_svn_log_entry(svn_start_log, svn_files) except: run_hg(["revert", "--all"]) raise # in the local case the files here already if not options.local_only: # Move SVN working copy here (don't forget base directory) for d in chain([""], svn_dirs): os.rename(os.path.join(checkout_dir, d, svn_private_dir), os.path.join(d, svn_private_dir)) finally: # in the local case the checkout_dir is the target, so don't delete it if not options.local_only: rmtree(checkout_dir) if options.hgignore: svn_ignores = [] # we use '.' because that it the location of the hg repo that we are # building and at this point we have already copied all of svn to the # checkout (for both the local and non-local case) for (path, dirs, files) in os.walk('.'): if '.svn' in dirs: dirs.remove('.svn') # the [2:] strips off the initial './' local_ignores = [os.path.join(path, s.strip())[2:] for s in run_svn(['propget', 'svn:ignore', path], mask_atsign=True).splitlines() if bool(s.strip()) ] svn_ignores += local_ignores else: # if we are not inside an svn world # we can just bail del dirs[:] # Generate .hgignore file to ignore .svn and .hgsvn directories f = open(".hgignore", "a") try: f.write("\n# Automatically generated by `hgimportsvn`\n") f.write("syntax:glob\n%s\n" % "\n".join([svn_private_dir, hgsvn_private_dir])) f.write("\n# These lines are suggested according to the svn:ignore property") f.write("\n# Feel free to enable them by uncommenting them") f.write("\nsyntax:glob\n") f.write("".join("#%s\n" % s for s in svn_ignores)) finally: f.close() print "Finished! You can now pull all SVN history with 'hgpullsvn'."
def get_hg_cset(rev_string): """ Given a string identifying an hg revision, get the canonical changeset ID. """ args = ["log", "--template", r"{rev}:{node|short}\n", "-r", rev_string] return run_hg(args).strip()
def get_hg_cset_description(rev_string): """Get description of a given changeset.""" return run_hg(["log", "--template", "{desc|strip}", "-r", rev_string])
def real_main(options, args): if check_for_applied_patches(): print("There are applied mq patches. Put them aside before " "running hgpullsvn.") return 1 svn_wc = "." # Get SVN info svn_info = get_svn_info('.') current_rev = svn_info['revision'] next_rev = current_rev + 1 # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted' repos_url = svn_info['repos_url'] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2' wc_url = svn_info['url'] assert wc_url.startswith(repos_url) # e.g. u'/branches/xmpp-subprotocols-2178-2' wc_base = wc_url[len(repos_url):] # e.g. 'xmpp-subprotocols-2178-2' svn_branch = wc_url.split("/")[-1] # if --branch was passed, override the branch name derived above if options.svn_branch: svn_branch = options.svn_branch if options.svn_peg: wc_url += "@" + str(options.svn_peg) # Get remote SVN info ui.status("Retrieving remote SVN info...", level=ui.VERBOSE) svn_greatest_rev = get_svn_info(wc_url)['last_changed_rev'] if svn_greatest_rev < next_rev: ui.status("No revisions after %s in SVN repo, nothing to do", svn_greatest_rev) return elif options.svn_rev != None: if options.svn_rev < next_rev: ui.status( "All revisions up to %s are already pulled in, " "nothing to do", options.svn_rev) return svn_greatest_rev = options.svn_rev # Show incoming changes if in dry-run mode. if options.dryrun: ui.status("Incoming SVN revisions:") for entry in iter_svn_log_entries(wc_url, next_rev, svn_greatest_rev, options.svnretry): if entry["message"]: msg = entry["message"].splitlines()[0].strip() else: msg = "" line = "[%d] %s (%s)" % (entry["revision"], msg, entry["author"]) ui.status(line) return # Prepare and/or switch named branches orig_branch = run_hg(["branch"]).strip() if orig_branch != svn_branch: # Update to or create the "pristine" branch if not hg_switch_branch(orig_branch, svn_branch): return 1 elif not hg_is_clean(svn_branch): return 1 # Detect that a previously aborted hgpullsvn retrieved an SVN revision # without committing it to hg. # If there is no SVN tag in current head, we may have been interrupted # during the previous "hg tag". hgsvn_rev = get_svn_rev_from_hg() if hgsvn_rev is not None and hgsvn_rev != current_rev: ui.status(("\nNote: hgsvn repository is in an unclean state " "(probably because of an aborted hgpullsvn). \n" "Let's first update to the last good SVN rev."), level=ui.VERBOSE) run_svn(["revert", "-R", "."]) run_svn(["up", "--ignore-externals", "-r", hgsvn_rev, svn_wc]) next_rev = hgsvn_rev + 1 # Reset working branch to last svn head to have a clean and linear SVN # history. heads_before = None if hgsvn_rev is None: heads_before = run_hg(["heads", "--template", "{node}%s" % os.linesep]).splitlines() run_hg(["update", "-C", "svn.%d" % current_rev]) # Load SVN log starting from current rev it_log_entries = iter_svn_log_entries(wc_url, next_rev, svn_greatest_rev, options.svnretry) try: try: for log_entry in it_log_entries: current_rev = pull_svn_rev(log_entry, current_rev, svn_wc, wc_url, wc_base, options.svnretry) if current_rev is None: return 1 ui.status("Pulled r%d %s (%s)", log_entry["revision"], log_entry["message"], log_entry["author"]) # TODO: detect externals with "svn status" and update them as well finally: if heads_before is not None: # we have reset the SVN branch heads_now = run_hg( ["heads", "--template", "{node}%s" % os.linesep]).splitlines() if len(heads_now) != len(heads_before): ui.status("created new head in branch '%s'", svn_branch) work_branch = orig_branch or svn_branch if work_branch != svn_branch: run_hg(["up", '-C', work_branch]) run_hg(["branch", work_branch]) except KeyboardInterrupt: ui.status("\nStopped by user.", level=ui.ERROR) except ExternalCommandFailed, e: ui.status(str(e), level=ui.ERROR)
def hg_push_svn(start_rev, end_rev, edit, username, password, cache): """ Commit the changes between two hg revisions into the SVN repository. Returns the SVN revision object, or None if nothing needed checking in. """ added, removed, modified, copied = get_hg_changes(start_rev+':'+end_rev) # Before replicating changes revert directory to previous state... run_hg(['revert', '--all', '--no-backup', '-r', end_rev]) # ... and restore .svn directories, if we lost some of them due to removes run_svn(['revert', '--recursive', '.']) # Do the rest over up-to-date working copy # Issue 94 - moved this line to prevent do_svn_copy crashing when its not the first changeset run_hg(["up", "-C", end_rev]) # Record copyies into svn for dest, src in copied.iteritems(): do_svn_copy(src,dest) # Add new files and dirs if added: bulk_args = get_ordered_dirs(added) + added run_svn(["add", "--depth=empty"], bulk_args, mask_atsign=True) removed = cleanup_svn_unversioned(removed) modified = cleanup_svn_unversioned(modified) # Remove old files and empty dirs if removed: empty_dirs = [d for d in reversed(get_ordered_dirs(removed)) if not run_hg(["st", "-c", "%s" %d])] # When running "svn rm" all files within removed folders needs to # be removed from "removed". Otherwise "svn rm" will fail. For example # instead of "svn rm foo/bar foo" it should be "svn rm foo". # See issue15. svn_removed = strip_nested_removes(removed + empty_dirs) run_svn(["rm"], svn_removed, mask_atsign=True) if added or removed or modified: svn_sep_line = "--This line, and those below, will be ignored--\n" adjust_executable_property(added+modified) # issue24 description = get_hg_csets_description(start_rev, end_rev) fname = os.path.join(hgsvn_private_dir, 'commit-%s.txt' % end_rev) lines = description.splitlines()+[""] lines.append(svn_sep_line) lines.append("To cancel commit just delete text in top message part") lines.append("") lines.append("Changes to be committed into svn:") lines.extend([item.decode("utf-8") for item in run_svn(["st", "-q"]).splitlines()]) lines.append("") lines.append(("These changes are produced from the following " "Hg changesets:")) lines.extend(get_hg_log(start_rev, end_rev).splitlines()) f = codecs.open(fname, "wb", "utf-8") f.write(os.linesep.join(lines)) f.close() try: if edit: editor=(os.environ.get("HGEDITOR") or os.environ.get("SVNEDITOR") or os.environ.get("VISUAL") or os.environ.get("EDITOR", "vi")) rc = os.system("%s \"%s\"" % (editor, fname) ) if(rc): raise ExternalCommandFailed("Can't launch editor") empty = True f=open(fname, "r") for line in f: if(line == svn_sep_line): break if(line.strip() != ""): empty = False break f.close() if(empty): raise EmptySVNLog("Commit cancelled by user\n") svn_rev = None svn_commands = ["commit", "-F", fname, "--encoding", get_encoding()] if username is not None: svn_commands += ["--username", username] if password is not None: svn_commands += ["--password", password] if cache: svn_commands.append("--no-auth-cache") out = run_svn(svn_commands) outlines = out.splitlines() outlines.reverse() for line in outlines: # one of the last lines holds the revision. # The previous approach to set LC_ALL to C and search # for "Committed revision XXX" doesn't work since # svn is unable to convert filenames containing special # chars: # http://svnbook.red-bean.com/en/1.2/svn.advanced.l10n.html match = re.search("(\d+)", line) if match: svn_rev = int(match.group(1)) break return svn_rev finally: # Exceptions are handled by main(). os.remove(fname) else: print "*", "svn: nothing to do" return None
def get_hg_csets_description(start_rev, end_rev): """Get description of a given changeset range.""" return run_hg([ "log", "--template", r"{desc|strip}\n", "--follow", "--rev", start_rev + ":" + end_rev, "--prune", start_rev ])
def pull_svn_rev(log_entry, current_rev, svn_wc, wc_url, wc_base, retry): """ Pull SVN changes from the given log entry. Returns the new SVN revision. If an exception occurs, it will rollback to revision 'current_rev'. """ svn_rev = log_entry['revision'] added_paths = [] copied_paths = [] removed_paths = [] changed_paths = [] unrelated_paths = [] replaced_paths = {} # 1. Prepare for the `svn up` changes that are pulled in the second step # by analyzing log_entry for the changeset for d in log_entry['changed_paths']: # e.g. u'/branches/xmpp-subprotocols-2178-2/twisted/words/test/test_jabberxmlstream.py' p = d['path'] if not p.startswith(wc_base + "/"): # Ignore changed files that are not part of this subdir if p != wc_base: unrelated_paths.append(p) continue action = d['action'] if action not in 'MARD': raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!" % (svn_rev, action)) # e.g. u'twisted/words/test/test_jabberxmlstream.py' p = p[len(wc_base):].strip("/") # Record for commit changed_paths.append(p) # Detect special cases old_p = d['copyfrom_path'] if old_p and old_p.startswith(wc_base + "/"): old_p = old_p[len(wc_base):].strip("/") # Both paths can be identical if copied from an old rev. # We treat like it a normal change. if old_p != p: # Try to hint hg about file and dir copies if not os.path.isdir(old_p): copied_paths.append((old_p, p)) if action == 'R': removed_paths.append(old_p) else: # Extract actual copied files (hg doesn't track dirs # and will refuse "hg copy -A" with dirs) r = run_hg(["st", "-nc"], [old_p], output_is_locale_encoding=True) for old_f in r.splitlines(): f = p + old_f[len(old_p):] copied_paths.append((old_f, f)) if action == 'R': removed_paths.append(old_f) continue if d['action'] == 'A': added_paths.append(p) elif d['action'] == 'D': # Same as above: unfold directories into their affected files if not os.path.isdir(p): removed_paths.append(p) else: r = run_hg(["st", "-nc"], [p], output_is_locale_encoding=True) for f in r.splitlines(): removed_paths.append(f) elif d['action'] == 'R': # (R)eplaced directories can have added and removed files without # them being mentioned in the SVN log => we must detect those files # ourselves. # (http://svn.python.org/projects/python/branches/py3k, rev 59625) if os.path.isdir(p): replaced_paths[p] = get_svn_versioned_files( os.path.join(svn_wc, p)) else: # We never know what twisty semantics (R) can have. addremove # is safest. added_paths.append(p) # 2. Update SVN + add/remove/commit hg try: if changed_paths: args = ["up", "--ignore-externals"] if get_svn_client_version() >= (1, 5): args.extend(['--accept', 'postpone']) ui.status('Attempting to update to revision %s...', str(svn_rev)) once_or_more("SVN update", retry, run_svn, args + ["-r", svn_rev, svn_wc]) conflicts = [] for status_entry in get_svn_status('.'): if status_entry['status'] == 'conflicted': conflicts.append(status_entry['path']) if conflicts: ui.status('SVN updated resulted in conflicts!', level=ui.ERROR) ui.status('Conflicted files: %s', ','.join(conflicts)) ui.status('Please report a bug.') return None for p, old_contents in replaced_paths.items(): old_contents = set(old_contents) new_contents = set(get_svn_versioned_files( os.path.join(svn_wc, p))) added_paths.extend(p + '/' + f for f in new_contents - old_contents) removed_paths.extend(p + '/' + f for f in old_contents - new_contents) if added_paths: # Use 'addremove' because an added SVN directory may # overwrite a previous directory with the same name. # XXX what about untracked files in those directories? run_hg(["addremove"] + hg_exclude_options, added_paths) for old, new in copied_paths: try: run_hg(["copy", "-A"], [old, new]) except ExternalCommandFailed: # Maybe the "old" path is obsolete, i.e. it comes from an # old SVN revision and was later removed. s = run_hg(['st', '-nd'], [old], output_is_locale_encoding=True) if s.strip(): # The old path is known by hg, something else happened. raise run_hg(["add"], [new]) if removed_paths: try: for file_path in removed_paths: if os.path.exists(file_path): run_hg(["remove", "-A"], file_path) except (ExternalCommandFailed), e: if str(e).find("file is untracked") > 0: ui.status("Ignoring warnings about untracked files: '%s'" % str(e), level=ui.VERBOSE) else: raise hg_commit_from_svn_log_entry(log_entry) elif unrelated_paths: detect_overwritten_svn_branch(wc_url, svn_rev)
def get_hg_log(start_rev, end_rev): """Get log messages for given changeset range.""" log_args=["log", "--verbose", "--follow", "--rev", start_rev+":"+end_rev, "--prune", start_rev] return run_hg(log_args)
def real_main(options, args): if check_for_applied_patches(): print ("There are applied mq patches. Put them aside before " "running hgpullsvn.") return 1 svn_wc = "." # Get SVN info svn_info = get_svn_info('.') current_rev = svn_info['revision'] next_rev = current_rev + 1 # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted' repos_url = svn_info['repos_url'] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2' wc_url = svn_info['url'] assert wc_url.startswith(repos_url) # e.g. u'/branches/xmpp-subprotocols-2178-2' wc_base = wc_url[len(repos_url):] # e.g. 'xmpp-subprotocols-2178-2' svn_branch = wc_url.split("/")[-1] # if --branch was passed, override the branch name derived above if options.svn_branch: svn_branch = options.svn_branch if options.svn_peg: wc_url += "@" + str(options.svn_peg) # Get remote SVN info ui.status("Retrieving remote SVN info...", level=ui.VERBOSE) svn_greatest_rev = get_svn_info(wc_url)['last_changed_rev'] if svn_greatest_rev < next_rev: ui.status("No revisions after %s in SVN repo, nothing to do", svn_greatest_rev) return elif options.svn_rev != None: if options.svn_rev < next_rev: ui.status("All revisions up to %s are already pulled in, " "nothing to do", options.svn_rev) return svn_greatest_rev = options.svn_rev # Show incoming changes if in dry-run mode. if options.dryrun: ui.status("Incoming SVN revisions:") for entry in iter_svn_log_entries(wc_url, next_rev, svn_greatest_rev, options.svnretry): if entry["message"]: msg = entry["message"].splitlines()[0].strip() else: msg = "" line = "[%d] %s (%s)" % (entry["revision"], msg, entry["author"]) ui.status(line) return # Prepare and/or switch named branches orig_branch = run_hg(["branch"]).strip() if orig_branch != svn_branch: # Update to or create the "pristine" branch if not hg_switch_branch(orig_branch, svn_branch): return 1 elif not hg_is_clean(svn_branch): return 1 # Detect that a previously aborted hgpullsvn retrieved an SVN revision # without committing it to hg. # If there is no SVN tag in current head, we may have been interrupted # during the previous "hg tag". hgsvn_rev = get_svn_rev_from_hg() if hgsvn_rev is not None and hgsvn_rev != current_rev: ui.status(("\nNote: hgsvn repository is in an unclean state " "(probably because of an aborted hgpullsvn). \n" "Let's first update to the last good SVN rev."), level=ui.VERBOSE) run_svn(["revert", "-R", "."]) run_svn(["up", "--ignore-externals", "-r", hgsvn_rev, svn_wc]) next_rev = hgsvn_rev + 1 # Reset working branch to last svn head to have a clean and linear SVN # history. heads_before = None if hgsvn_rev is None: heads_before = run_hg(["heads", "--template", "{node}%s" % os.linesep]).splitlines() run_hg(["update", "-C", "svn.%d" % current_rev]) # Load SVN log starting from current rev it_log_entries = iter_svn_log_entries(wc_url, next_rev, svn_greatest_rev, options.svnretry) try: try: for log_entry in it_log_entries: current_rev = pull_svn_rev(log_entry, current_rev, svn_wc, wc_url, wc_base, options.svnretry) if current_rev is None: return 1 ui.status("Pulled r%d %s (%s)", log_entry["revision"], log_entry["message"], log_entry["author"]) # TODO: detect externals with "svn status" and update them as well finally: if heads_before is not None: # we have reset the SVN branch heads_now = run_hg(["heads", "--template", "{node}%s" % os.linesep]).splitlines() if len(heads_now) != len(heads_before): ui.status("created new head in branch '%s'", svn_branch) work_branch = orig_branch or svn_branch if work_branch != svn_branch: run_hg(["up", '-C', work_branch]) run_hg(["branch", work_branch]) except KeyboardInterrupt: ui.status("\nStopped by user.", level=ui.ERROR) except ExternalCommandFailed, e: ui.status(str(e), level=ui.ERROR)
def hg_push_svn(start_rev, end_rev, edit, username, password, cache): """ Commit the changes between two hg revisions into the SVN repository. Returns the SVN revision object, or None if nothing needed checking in. """ added, removed, modified, copied = get_hg_changes(start_rev + ':' + end_rev) # Before replicating changes revert directory to previous state... run_hg(['revert', '--all', '--no-backup', '-r', end_rev]) # ... and restore .svn directories, if we lost some of them due to removes run_svn(['revert', '--recursive', '.']) # Do the rest over up-to-date working copy # Issue 94 - moved this line to prevent do_svn_copy crashing when its not the first changeset run_hg(["up", "-C", end_rev]) # Record copyies into svn for dest, src in copied.iteritems(): do_svn_copy(src, dest) # Add new files and dirs if added: bulk_args = get_ordered_dirs(added) + added run_svn(["add", "--depth=empty"], bulk_args, mask_atsign=True) removed = cleanup_svn_unversioned(removed) modified = cleanup_svn_unversioned(modified) # Remove old files and empty dirs if removed: empty_dirs = [ d for d in reversed(get_ordered_dirs(removed)) if not run_hg(["st", "-c", "%s" % d]) ] # When running "svn rm" all files within removed folders needs to # be removed from "removed". Otherwise "svn rm" will fail. For example # instead of "svn rm foo/bar foo" it should be "svn rm foo". # See issue15. svn_removed = strip_nested_removes(removed + empty_dirs) run_svn(["rm"], svn_removed, mask_atsign=True) if added or removed or modified: svn_sep_line = "--This line, and those below, will be ignored--\n" adjust_executable_property(added + modified) # issue24 description = get_hg_csets_description(start_rev, end_rev) fname = os.path.join(hgsvn_private_dir, 'commit-%s.txt' % end_rev) lines = description.splitlines() + [""] lines.append(svn_sep_line) lines.append("To cancel commit just delete text in top message part") lines.append("") lines.append("Changes to be committed into svn:") lines.extend([ item.decode("utf-8") for item in run_svn(["st", "-q"]).splitlines() ]) lines.append("") lines.append(("These changes are produced from the following " "Hg changesets:")) lines.extend(get_hg_log(start_rev, end_rev).splitlines()) f = codecs.open(fname, "wb", "utf-8") f.write(os.linesep.join(lines)) f.close() try: if edit: editor = (os.environ.get("HGEDITOR") or os.environ.get("SVNEDITOR") or os.environ.get("VISUAL") or os.environ.get("EDITOR", "vi")) rc = os.system("%s \"%s\"" % (editor, fname)) if (rc): raise ExternalCommandFailed("Can't launch editor") empty = True f = open(fname, "r") for line in f: if (line == svn_sep_line): break if (line.strip() != ""): empty = False break f.close() if (empty): raise EmptySVNLog("Commit cancelled by user\n") svn_rev = None svn_commands = [ "commit", "-F", fname, "--encoding", get_encoding() ] if username is not None: svn_commands += ["--username", username] if password is not None: svn_commands += ["--password", password] if cache: svn_commands.append("--no-auth-cache") out = run_svn(svn_commands) outlines = out.splitlines() outlines.reverse() for line in outlines: # one of the last lines holds the revision. # The previous approach to set LC_ALL to C and search # for "Committed revision XXX" doesn't work since # svn is unable to convert filenames containing special # chars: # http://svnbook.red-bean.com/en/1.2/svn.advanced.l10n.html match = re.search("(\d+)", line) if match: svn_rev = int(match.group(1)) break return svn_rev finally: # Exceptions are handled by main(). os.remove(fname) else: print "*", "svn: nothing to do" return None
raise hg_commit_from_svn_log_entry(log_entry) elif unrelated_paths: detect_overwritten_svn_branch(wc_url, svn_rev) # NOTE: in Python 2.5, KeyboardInterrupt isn't a subclass of Exception anymore except (Exception, KeyboardInterrupt), e: ui.status("\nInterrupted, please wait for cleanup!\n", level=ui.ERROR) # NOTE: at this point, running SVN sometimes gives "write lock failures" # which leave the WC in a weird state. time.sleep(0.3) run_svn(["cleanup"]) hgsvn_rev = get_svn_rev_from_hg() if hgsvn_rev != svn_rev: # Unless the tag is there, revert to the last stable state run_svn(["up", "--ignore-externals", "-r", current_rev, svn_wc]) run_hg(["revert", "--all"]) raise return svn_rev def real_main(options, args): if check_for_applied_patches(): print("There are applied mq patches. Put them aside before " "running hgpullsvn.") return 1 svn_wc = "." # Get SVN info svn_info = get_svn_info('.') current_rev = svn_info['revision']
def real_main(options, args): if run_hg(["st", "-m"]): print( "There are uncommitted changes. Either commit them or put " "them aside before running hgpushsvn.") return 1 if check_for_applied_patches(): print( "There are applied mq patches. Put them aside before " "running hgpushsvn.") return 1 svn_info = get_svn_info(".") svn_current_rev = svn_info["last_changed_rev"] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted' repos_url = svn_info["repos_url"] # e.g. u'svn://svn.twistedmatrix.com/svn/Twisted/branches/xmpp-subprotocols-2178-2' wc_url = svn_info["url"] assert wc_url.startswith(repos_url) # e.g. u'/branches/xmpp-subprotocols-2178-2' wc_base = wc_url[len(repos_url):] svn_branch = wc_url.split("/")[-1] # Get remote SVN info svn_greatest_rev = get_svn_info(wc_url)['last_changed_rev'] if svn_greatest_rev != svn_current_rev: # We can't go on if the pristine branch isn't up to date. # If the pristine branch lacks some revisions from SVN we are not # able to pull them afterwards. # For example, if the last SVN revision in out hgsvn repository is # r100 and the latest SVN revision is r101, hgpushsvn would create # a tag svn.102 on top of svn.100, but svn.101 isn't in hg history. print("Branch '%s' out of date. Run 'hgpullsvn' first." % svn_branch) return 1 # Switch branches if necessary. orig_branch = run_hg(["branch"]).strip() if orig_branch != svn_branch: if not hg_switch_branch(orig_branch, svn_branch): return 1 hg_start_rev = "svn.%d" % svn_current_rev hg_revs = None try: hg_start_cset = get_hg_cset(hg_start_rev) except RuntimeError: if not options.force: raise hg_start_cset = get_hg_cset("0") print "Warning: revision '%s' not found, forcing to first rev '%s'" % ( hg_start_rev, hg_start_cset) else: if not options.collapse: hg_revs = get_hg_revs(hg_start_cset, svn_branch) if hg_revs is None: hg_revs = [ strip_hg_rev(hg_start_cset), strip_hg_rev(get_hg_cset("tip")) ] pushed_svn_revs = [] try: if options.dryrun: print "Outgoing revisions that would be pushed to SVN:" try: for prev_rev, next_rev in get_pairs(hg_revs): if not options.dryrun: if not options.edit: ui.status("Committing changes up to revision %s", get_hg_cset(next_rev)) username = options.username if options.keep_author: username = run_hg( ["log", "-r", next_rev, "--template", "{author}"]) svn_rev = hg_push_svn(prev_rev, next_rev, edit=options.edit, username=username, password=options.password, cache=options.cache) if svn_rev: # Issue 95 - added update to prevent add/modify/delete crash run_svn(["up"]) map_svn_rev_to_hg(svn_rev, next_rev, local=True) pushed_svn_revs.append(svn_rev) else: print run_hg([ "log", "-r", next_rev, "--template", "{rev}:{node|short} {desc}" ]) except: # TODO: Add --no-backup to leave a "clean" repo behind if something # fails? run_hg(["revert", "--all"]) raise finally: work_branch = orig_branch or svn_branch if work_branch != svn_branch: run_hg(["up", "-C", work_branch]) run_hg(["branch", work_branch]) if pushed_svn_revs: if len(pushed_svn_revs) == 1: msg = "Pushed one revision to SVN: " else: msg = "Pushed %d revisions to SVN: " % len(pushed_svn_revs) run_svn(["up", "-r", pushed_svn_revs[-1]]) ui.status("%s %s", msg, ", ".join(str(x) for x in pushed_svn_revs)) for line in run_hg(["st"]).splitlines(): if line.startswith("M"): ui.status(("Mercurial repository has local changes after " "SVN update.")) ui.status(("This may happen with SVN keyword expansions.")) break elif not options.dryrun: ui.status("Nothing to do.")
def pull_svn_rev(log_entry, current_rev, svn_wc, wc_url, wc_base, retry): """ Pull SVN changes from the given log entry. Returns the new SVN revision. If an exception occurs, it will rollback to revision 'current_rev'. """ svn_rev = log_entry['revision'] added_paths = [] copied_paths = [] removed_paths = [] changed_paths = [] unrelated_paths = [] replaced_paths = {} # 1. Prepare for the `svn up` changes that are pulled in the second step # by analyzing log_entry for the changeset for d in log_entry['changed_paths']: # e.g. u'/branches/xmpp-subprotocols-2178-2/twisted/words/test/test_jabberxmlstream.py' p = d['path'] if not p.startswith(wc_base + "/"): # Ignore changed files that are not part of this subdir if p != wc_base: unrelated_paths.append(p) continue action = d['action'] if action not in 'MARD': raise UnsupportedSVNAction( "In SVN rev. %d: action '%s' not supported. Please report a bug!" % (svn_rev, action)) # e.g. u'twisted/words/test/test_jabberxmlstream.py' p = p[len(wc_base):].strip("/") # Record for commit changed_paths.append(p) # Detect special cases old_p = d['copyfrom_path'] if old_p and old_p.startswith(wc_base + "/"): old_p = old_p[len(wc_base):].strip("/") # Both paths can be identical if copied from an old rev. # We treat like it a normal change. if old_p != p: # Try to hint hg about file and dir copies if not os.path.isdir(old_p): copied_paths.append((old_p, p)) if action == 'R': removed_paths.append(old_p) else: # Extract actual copied files (hg doesn't track dirs # and will refuse "hg copy -A" with dirs) r = run_hg(["st", "-nc"], [old_p], output_is_locale_encoding=True) for old_f in r.splitlines(): f = p + old_f[len(old_p):] copied_paths.append((old_f, f)) if action == 'R': removed_paths.append(old_f) continue if d['action'] == 'A': added_paths.append(p) elif d['action'] == 'D': # Same as above: unfold directories into their affected files if not os.path.isdir(p): removed_paths.append(p) else: r = run_hg(["st", "-nc"], [p], output_is_locale_encoding=True) for f in r.splitlines(): removed_paths.append(f) elif d['action'] == 'R': # (R)eplaced directories can have added and removed files without # them being mentioned in the SVN log => we must detect those files # ourselves. # (http://svn.python.org/projects/python/branches/py3k, rev 59625) if os.path.isdir(p): replaced_paths[p] = get_svn_versioned_files( os.path.join(svn_wc, p)) else: # We never know what twisty semantics (R) can have. addremove # is safest. added_paths.append(p) # 2. Update SVN + add/remove/commit hg try: if changed_paths: args = ["up", "--ignore-externals"] if get_svn_client_version() >= (1, 5): args.extend(['--accept', 'postpone']) ui.status('Attempting to update to revision %s...', str(svn_rev)) once_or_more("SVN update", retry, run_svn, args + ["-r", svn_rev, svn_wc]) conflicts = [] for status_entry in get_svn_status('.'): if status_entry['status'] == 'conflicted': conflicts.append(status_entry['path']) if conflicts: ui.status('SVN updated resulted in conflicts!', level=ui.ERROR) ui.status('Conflicted files: %s', ','.join(conflicts)) ui.status('Please report a bug.') return None for p, old_contents in replaced_paths.items(): old_contents = set(old_contents) new_contents = set( get_svn_versioned_files(os.path.join(svn_wc, p))) added_paths.extend(p + '/' + f for f in new_contents - old_contents) removed_paths.extend(p + '/' + f for f in old_contents - new_contents) if added_paths: # Use 'addremove' because an added SVN directory may # overwrite a previous directory with the same name. # XXX what about untracked files in those directories? run_hg(["addremove"] + hg_exclude_options, added_paths) for old, new in copied_paths: try: run_hg(["copy", "-A"], [old, new]) except ExternalCommandFailed: # Maybe the "old" path is obsolete, i.e. it comes from an # old SVN revision and was later removed. s = run_hg(['st', '-nd'], [old], output_is_locale_encoding=True) if s.strip(): # The old path is known by hg, something else happened. raise run_hg(["add"], [new]) if removed_paths: try: for file_path in removed_paths: if os.path.exists(file_path): run_hg(["remove", "-A"], file_path) except (ExternalCommandFailed), e: if str(e).find("file is untracked") > 0: ui.status( "Ignoring warnings about untracked files: '%s'" % str(e), level=ui.VERBOSE) else: raise hg_commit_from_svn_log_entry(log_entry) elif unrelated_paths: detect_overwritten_svn_branch(wc_url, svn_rev)
def get_hg_csets_description(start_rev, end_rev): """Get description of a given changeset range.""" return run_hg(["log", "--template", r"{desc|strip}\n", "--follow", "--rev", start_rev+":"+end_rev, "--prune", start_rev])