(original) (raw)

#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2005, Giovanni Bajo # Copyright (c) 2004-2005, Awarix, Inc. # All rights reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # # Author: Archie Cobbs # Rewritten in Python by: Giovanni Bajo # # Acknowledgments: # John Belmonte - metadata and usability # improvements # Blair Zajac - random improvements # Raman Gupta - bidirectional and transitive # merging support # Dustin J. Mitchell - support for multiple # location identifier formats # # HeadURLHeadURLHeadURL # LastChangedDateLastChangedDateLastChangedDate # LastChangedByLastChangedByLastChangedBy # LastChangedRevisionLastChangedRevisionLastChangedRevision # # Requisites: # svnmerge.py has been tested with all SVN major versions since 1.1 (both # client and server). It is unknown if it works with previous versions. # # Differences from svnmerge.sh: # - More portable: tested as working in FreeBSD and OS/2. # - Add double-verbose mode, which shows every svn command executed (-v -v). # - "svnmerge avail" now only shows commits in source, not also commits in # other parts of the repository. # - Add "svnmerge block" to flag some revisions as blocked, so that # they will not show up anymore in the available list. Added also # the complementary "svnmerge unblock". # - "svnmerge avail" has grown two new options: # -B to display a list of the blocked revisions # -A to display both the blocked and the available revisions. # - Improved generated commit message to make it machine parsable even when # merging commits which are themselves merges. # - Add --force option to skip working copy check # - Add --record-only option to "svnmerge merge" to avoid performing # an actual merge, yet record that a merge happened. # - Can use a variety of location-identifier formats # # TODO: # - Add "svnmerge avail -R": show logs in reverse order # # Information for Hackers: # # Identifiers for branches: # A branch is identified in three ways within this source: # - as a working copy (variable name usually includes 'dir') # - as a fully qualified URL # - as a path identifier (an opaque string indicating a particular path # in a particular repository; variable name includes 'pathid') # A "target" is generally user-specified, and may be a working copy or # a URL. import sys, os, getopt, re, types, tempfile, time, locale from bisect import bisect from xml.dom import pulldom NAME = "svnmerge" if not hasattr(sys, "version_info") or sys.version_info < (2, 0): error("requires Python 2.0 or newer") # Set up the separator used to separate individual log messages from # each revision merged into the target location. Also, create a # regular expression that will find this same separator in already # committed log messages, so that the separator used for this run of # svnmerge.py will have one more LOG_SEPARATOR appended to the longest # separator found in all the commits. LOG_SEPARATOR = 8 * '.' LOG_SEPARATOR_RE = re.compile('^((%s)+)' % re.escape(LOG_SEPARATOR), re.MULTILINE) # Each line of the embedded log messages will be prefixed by LOG_LINE_PREFIX. LOG_LINE_PREFIX = 2 * ' ' # Set python to the default locale as per environment settings, same as svn # TODO we should really parse config and if log-encoding is specified, set # the locale to match that encoding locale.setlocale(locale.LC_ALL, '') # We want the svn output (such as svn info) to be non-localized # Using LC_MESSAGES should not affect localized output of svn log, for example if os.environ.has_key("LC_ALL"): del os.environ["LC_ALL"] os.environ["LC_MESSAGES"] = "C" ############################################################################### # Support for older Python versions ############################################################################### # True/False constants are Python 2.2+ try: True, False except NameError: True, False = 1, 0 def lstrip(s, ch): """Replacement for str.lstrip (support for arbitrary chars to strip was added in Python 2.2.2).""" i = 0 try: while s[i] == ch: i = i+1 return s[i:] except IndexError: return "" def rstrip(s, ch): """Replacement for str.rstrip (support for arbitrary chars to strip was added in Python 2.2.2).""" try: if s[-1] != ch: return s i = -2 while s[i] == ch: i = i-1 return s[:i+1] except IndexError: return "" def strip(s, ch): """Replacement for str.strip (support for arbitrary chars to strip was added in Python 2.2.2).""" return lstrip(rstrip(s, ch), ch) def rsplit(s, sep, maxsplits=0): """Like str.rsplit, which is Python 2.4+ only.""" L = s.split(sep) if not 0 < maxsplits <= len(L): return L return [sep.join(L[0:-maxsplits])] + L[-maxsplits:] ############################################################################### def kwextract(s): """Extract info from a svn keyword string.""" try: return strip(s, "$").strip().split(": ")[1] except IndexError: return "" __revision__ = kwextract('$Rev$') __date__ = kwextract('$Date$') # Additional options, not (yet?) mapped to command line flags default_opts = { "svn": "svn", "prop": NAME + "-integrated", "block-prop": NAME + "-blocked", "commit-verbose": True, "verbose": 0, } logs = {} def console_width(): """Get the width of the console screen (if any).""" try: return int(os.environ["COLUMNS"]) except (KeyError, ValueError): pass try: # Call the Windows API (requires ctypes library) from ctypes import windll, create_string_buffer h = windll.kernel32.GetStdHandle(-11) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: import struct (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) return right - left + 1 except ImportError: pass # Parse the output of stty -a if os.isatty(1): out = os.popen("stty -a").read() m = re.search(r"columns (\d+);", out) if m: return int(m.group(1)) # sensible default return 80 def error(s): """Subroutine to output an error and bail.""" sys.stderr.write("%s: %s\n" % (NAME, s)) sys.exit(1) def report(s): """Subroutine to output progress message, unless in quiet mode.""" if opts["verbose"]: print("%s: %s" % (NAME, s)) def prefix_lines(prefix, lines): """Given a string representing one or more lines of text, insert the specified prefix at the beginning of each line, and return the result. The input must be terminated by a newline.""" assert lines[-1] == "\n" return prefix + lines[:-1].replace("\n", "\n"+prefix) + "\n" def recode_stdout_to_file(s): if locale.getdefaultlocale()[1] is None or not hasattr(sys.stdout, "encoding") \ or sys.stdout.encoding is None: return s u = s.decode(sys.stdout.encoding) return u.encode(locale.getdefaultlocale()[1]) class LaunchError(Exception): """Signal a failure in execution of an external command. Parameters are the exit code of the process, the original command line, and the output of the command.""" try: """Launch a sub-process. Return its output (both stdout and stderr), optionally split by lines (if split_lines is True). Raise a LaunchError exception if the exit code of the process is non-zero (failure). This function has two implementations, one based on subprocess (preferred), and one based on popen (for compatibility). """ import subprocess import shlex def launch(cmd, split_lines=True): # Requiring python 2.4 or higher, on some platforms we get # much faster performance from the subprocess module (where python # doesn't try to close an exhorbitant number of file descriptors) stdout = "" stderr = "" try: if os.name == 'nt': p = subprocess.Popen(cmd, stdout=subprocess.PIPE, \ close_fds=False, stderr=subprocess.PIPE) else: # Use shlex to break up the parameters intelligently, # respecting quotes. shlex can't handle unicode. args = shlex.split(cmd.encode('ascii')) p = subprocess.Popen(args, stdout=subprocess.PIPE, \ close_fds=False, stderr=subprocess.PIPE) stdoutAndErr = p.communicate() stdout = stdoutAndErr[0] stderr = stdoutAndErr[1] except OSError as inst: # Using 1 as failure code; should get actual number somehow? For # examples see svnmerge_test.py's TestCase_launch.test_failure and # TestCase_launch.test_failurecode. raise LaunchError(1, cmd, stdout + " " + stderr + ": " + str(inst)) if p.returncode == 0: if split_lines: # Setting keepends=True for compatibility with previous logic # (where file.readlines() preserves newlines) return stdout.splitlines(True) else: return stdout else: raise LaunchError(p.returncode, cmd, stdout + stderr) except ImportError: # support versions of python before 2.4 (slower on some systems) def launch(cmd, split_lines=True): if os.name not in ['nt', 'os2']: import popen2 p = popen2.Popen4(cmd) p.tochild.close() if split_lines: out = p.fromchild.readlines() else: out = p.fromchild.read() ret = p.wait() if ret == 0: ret = None else: ret >>= 8 else: i,k = os.popen4(cmd) i.close() if split_lines: out = k.readlines() else: out = k.read() ret = k.close() if ret is None: return out raise LaunchError(ret, cmd, out) def launchsvn(s, show=False, pretend=False, **kwargs): """Launch SVN and grab its output.""" username = password = configdir = "" if opts.get("username", None): username = "--username=" + opts["username"] if opts.get("password", None): password = "--password=" + opts["password"] if opts.get("config-dir", None): configdir = "--config-dir=" + opts["config-dir"] cmd = ' '.join(filter(None, [opts["svn"], "--non-interactive", username, password, configdir, s])) if show or opts["verbose"] >= 2: print(cmd) if pretend: return None return launch(cmd, **kwargs) def svn_command(s): """Do (or pretend to do) an SVN command.""" out = launchsvn(s, show=opts["show-changes"] or opts["dry-run"], pretend=opts["dry-run"], split_lines=False) if not opts["dry-run"]: print(out) def check_dir_clean(dir): """Check the current status of dir for local mods.""" if opts["force"]: report('skipping status check because of --force') return report('checking status of "%s"' % dir) # Checking with -q does not show unversioned files or external # directories. Though it displays a debug message for external # directories, after a blank line. So, practically, the first line # matters: if it's non-empty there is a modification. (Lines starting # with "X" must be skipped, since they just indicate externals.) out = launchsvn("status -q %s" % dir) while out and out[0].strip(): if not out[0].startswith("X"): error('"%s" has local modifications; it must be clean' % dir) out.pop(0) class PathIdentifier: """Abstraction for a path identifier, so that we can start talking about it before we know the form that it takes in the properties (its external_form). Objects are referenced in the class variable 'locobjs', keyed by all known forms.""" # a map of UUID (or None) to repository root URL. repo_hints = {} # a map from any known string form to the corresponding PathIdentifier locobjs = {} def __init__(self, repo_relative_path, uuid=None, url=None, external_form=None): self.repo_relative_path = repo_relative_path self.uuid = uuid self.url = url self.external_form = external_form def __repr__(self): return "