scripts/svnmerge.py
changeset 64 b73eec62825a
equal deleted inserted replaced
63:9b1909e46633 64:b73eec62825a
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 # Copyright (c) 2005, Giovanni Bajo
       
     4 # Copyright (c) 2004-2005, Awarix, Inc.
       
     5 # All rights reserved.
       
     6 #
       
     7 # This program is free software; you can redistribute it and/or
       
     8 # modify it under the terms of the GNU General Public License
       
     9 # as published by the Free Software Foundation; either version 2
       
    10 # of the License, or (at your option) any later version.
       
    11 #
       
    12 # This program is distributed in the hope that it will be useful,
       
    13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    15 # GNU General Public License for more details.
       
    16 #
       
    17 # You should have received a copy of the GNU General Public License
       
    18 # along with this program; if not, write to the Free Software
       
    19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
       
    20 #
       
    21 # Author: Archie Cobbs <archie at awarix dot com>
       
    22 # Rewritten in Python by: Giovanni Bajo <rasky at develer dot com>
       
    23 #
       
    24 # Acknowledgments:
       
    25 #   John Belmonte <john at neggie dot net> - metadata and usability
       
    26 #     improvements
       
    27 #   Blair Zajac <blair at orcaware dot com> - random improvements
       
    28 #   Raman Gupta <rocketraman at fastmail dot fm> - bidirectional and transitive
       
    29 #     merging support
       
    30 #
       
    31 # $HeadURL$
       
    32 # $LastChangedDate$
       
    33 # $LastChangedBy$
       
    34 # $LastChangedRevision$
       
    35 #
       
    36 # Requisites:
       
    37 # svnmerge.py has been tested with all SVN major versions since 1.1 (both
       
    38 # client and server). It is unknown if it works with previous versions.
       
    39 #
       
    40 # Differences from svnmerge.sh:
       
    41 # - More portable: tested as working in FreeBSD and OS/2.
       
    42 # - Add double-verbose mode, which shows every svn command executed (-v -v).
       
    43 # - "svnmerge avail" now only shows commits in source, not also commits in
       
    44 #   other parts of the repository.
       
    45 # - Add "svnmerge block" to flag some revisions as blocked, so that
       
    46 #   they will not show up anymore in the available list.  Added also
       
    47 #   the complementary "svnmerge unblock".
       
    48 # - "svnmerge avail" has grown two new options:
       
    49 #   -B to display a list of the blocked revisions
       
    50 #   -A to display both the blocked and the available revisions.
       
    51 # - Improved generated commit message to make it machine parsable even when
       
    52 #   merging commits which are themselves merges.
       
    53 # - Add --force option to skip working copy check
       
    54 # - Add --record-only option to "svnmerge merge" to avoid performing
       
    55 #   an actual merge, yet record that a merge happened.
       
    56 #
       
    57 # TODO:
       
    58 #  - Add "svnmerge avail -R": show logs in reverse order
       
    59 #
       
    60 # Information for Hackers:
       
    61 #
       
    62 # Identifiers for branches:
       
    63 #  A branch is identified in three ways within this source:
       
    64 #  - as a working copy (variable name usually includes 'dir')
       
    65 #  - as a fully qualified URL
       
    66 #  - as a path identifier (an opaque string indicating a particular path
       
    67 #    in a particular repository; variable name includes 'pathid')
       
    68 #  A "target" is generally user-specified, and may be a working copy or
       
    69 #  a URL.
       
    70 
       
    71 import sys, os, getopt, re, types, tempfile, time, popen2, locale
       
    72 from bisect import bisect
       
    73 from xml.dom import pulldom
       
    74 
       
    75 NAME = "svnmerge"
       
    76 if not hasattr(sys, "version_info") or sys.version_info < (2, 0):
       
    77     error("requires Python 2.0 or newer")
       
    78 
       
    79 # Set up the separator used to separate individual log messages from
       
    80 # each revision merged into the target location.  Also, create a
       
    81 # regular expression that will find this same separator in already
       
    82 # committed log messages, so that the separator used for this run of
       
    83 # svnmerge.py will have one more LOG_SEPARATOR appended to the longest
       
    84 # separator found in all the commits.
       
    85 LOG_SEPARATOR = 8 * '.'
       
    86 LOG_SEPARATOR_RE = re.compile('^((%s)+)' % re.escape(LOG_SEPARATOR),
       
    87                               re.MULTILINE)
       
    88 
       
    89 # Each line of the embedded log messages will be prefixed by LOG_LINE_PREFIX.
       
    90 LOG_LINE_PREFIX = 2 * ' '
       
    91 
       
    92 # Set python to the default locale as per environment settings, same as svn
       
    93 # TODO we should really parse config and if log-encoding is specified, set
       
    94 # the locale to match that encoding
       
    95 locale.setlocale(locale.LC_ALL, '')
       
    96 
       
    97 # We want the svn output (such as svn info) to be non-localized
       
    98 # Using LC_MESSAGES should not affect localized output of svn log, for example
       
    99 if os.environ.has_key("LC_ALL"):
       
   100     del os.environ["LC_ALL"]
       
   101 os.environ["LC_MESSAGES"] = "C"
       
   102 
       
   103 ###############################################################################
       
   104 # Support for older Python versions
       
   105 ###############################################################################
       
   106 
       
   107 # True/False constants are Python 2.2+
       
   108 try:
       
   109     True, False
       
   110 except NameError:
       
   111     True, False = 1, 0
       
   112 
       
   113 def lstrip(s, ch):
       
   114     """Replacement for str.lstrip (support for arbitrary chars to strip was
       
   115     added in Python 2.2.2)."""
       
   116     i = 0
       
   117     try:
       
   118         while s[i] == ch:
       
   119             i = i+1
       
   120         return s[i:]
       
   121     except IndexError:
       
   122         return ""
       
   123 
       
   124 def rstrip(s, ch):
       
   125     """Replacement for str.rstrip (support for arbitrary chars to strip was
       
   126     added in Python 2.2.2)."""
       
   127     try:
       
   128         if s[-1] != ch:
       
   129             return s
       
   130         i = -2
       
   131         while s[i] == ch:
       
   132             i = i-1
       
   133         return s[:i+1]
       
   134     except IndexError:
       
   135         return ""
       
   136 
       
   137 def strip(s, ch):
       
   138     """Replacement for str.strip (support for arbitrary chars to strip was
       
   139     added in Python 2.2.2)."""
       
   140     return lstrip(rstrip(s, ch), ch)
       
   141 
       
   142 def rsplit(s, sep, maxsplits=0):
       
   143     """Like str.rsplit, which is Python 2.4+ only."""
       
   144     L = s.split(sep)
       
   145     if not 0 < maxsplits <= len(L):
       
   146         return L
       
   147     return [sep.join(L[0:-maxsplits])] + L[-maxsplits:]
       
   148 
       
   149 ###############################################################################
       
   150 
       
   151 def kwextract(s):
       
   152     """Extract info from a svn keyword string."""
       
   153     try:
       
   154         return strip(s, "$").strip().split(": ")[1]
       
   155     except IndexError:
       
   156         return "<unknown>"
       
   157 
       
   158 __revision__ = kwextract('$Rev$')
       
   159 __date__ = kwextract('$Date$')
       
   160 
       
   161 # Additional options, not (yet?) mapped to command line flags
       
   162 default_opts = {
       
   163     "svn": "svn",
       
   164     "prop": NAME + "-integrated",
       
   165     "block-prop": NAME + "-blocked",
       
   166     "commit-verbose": True,
       
   167 }
       
   168 logs = {}
       
   169 
       
   170 def console_width():
       
   171     """Get the width of the console screen (if any)."""
       
   172     try:
       
   173         return int(os.environ["COLUMNS"])
       
   174     except (KeyError, ValueError):
       
   175         pass
       
   176 
       
   177     try:
       
   178         # Call the Windows API (requires ctypes library)
       
   179         from ctypes import windll, create_string_buffer
       
   180         h = windll.kernel32.GetStdHandle(-11)
       
   181         csbi = create_string_buffer(22)
       
   182         res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
       
   183         if res:
       
   184             import struct
       
   185             (bufx, bufy,
       
   186              curx, cury, wattr,
       
   187              left, top, right, bottom,
       
   188              maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
       
   189             return right - left + 1
       
   190     except ImportError:
       
   191         pass
       
   192 
       
   193     # Parse the output of stty -a
       
   194     out = os.popen("stty -a").read()
       
   195     m = re.search(r"columns (\d+);", out)
       
   196     if m:
       
   197         return int(m.group(1))
       
   198 
       
   199     # sensible default
       
   200     return 80
       
   201 
       
   202 def error(s):
       
   203     """Subroutine to output an error and bail."""
       
   204     print >> sys.stderr, "%s: %s" % (NAME, s)
       
   205     sys.exit(1)
       
   206 
       
   207 def report(s):
       
   208     """Subroutine to output progress message, unless in quiet mode."""
       
   209     if opts["verbose"]:
       
   210         print "%s: %s" % (NAME, s)
       
   211 
       
   212 def prefix_lines(prefix, lines):
       
   213     """Given a string representing one or more lines of text, insert the
       
   214     specified prefix at the beginning of each line, and return the result.
       
   215     The input must be terminated by a newline."""
       
   216     assert lines[-1] == "\n"
       
   217     return prefix + lines[:-1].replace("\n", "\n"+prefix) + "\n"
       
   218 
       
   219 def recode_stdout_to_file(s):
       
   220     if locale.getdefaultlocale()[1] is None or not hasattr(sys.stdout, "encoding") \
       
   221             or sys.stdout.encoding is None:
       
   222         return s
       
   223     u = s.decode(sys.stdout.encoding)
       
   224     return u.encode(locale.getdefaultlocale()[1])
       
   225 
       
   226 class LaunchError(Exception):
       
   227     """Signal a failure in execution of an external command. Parameters are the
       
   228     exit code of the process, the original command line, and the output of the
       
   229     command."""
       
   230 
       
   231 try:
       
   232     """Launch a sub-process. Return its output (both stdout and stderr),
       
   233     optionally split by lines (if split_lines is True). Raise a LaunchError
       
   234     exception if the exit code of the process is non-zero (failure).
       
   235 
       
   236     This function has two implementations, one based on subprocess (preferred),
       
   237     and one based on popen (for compatibility).
       
   238     """
       
   239     import subprocess
       
   240     import shlex
       
   241 
       
   242     def launch(cmd, split_lines=True):
       
   243         # Requiring python 2.4 or higher, on some platforms we get
       
   244         # much faster performance from the subprocess module (where python
       
   245         # doesn't try to close an exhorbitant number of file descriptors)
       
   246         stdout = ""
       
   247         stderr = ""
       
   248         try:
       
   249             if os.name == 'nt':
       
   250                 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, \
       
   251                                      close_fds=False, stderr=subprocess.PIPE)
       
   252             else:
       
   253                 # Use shlex to break up the parameters intelligently,
       
   254                 # respecting quotes. shlex can't handle unicode.
       
   255                 args = shlex.split(cmd.encode('ascii'))
       
   256                 p = subprocess.Popen(args, stdout=subprocess.PIPE, \
       
   257                                      close_fds=False, stderr=subprocess.PIPE)
       
   258             stdoutAndErr = p.communicate()
       
   259             stdout = stdoutAndErr[0]
       
   260             stderr = stdoutAndErr[1]
       
   261         except OSError, inst:
       
   262             # Using 1 as failure code; should get actual number somehow? For
       
   263             # examples see svnmerge_test.py's TestCase_launch.test_failure and
       
   264             # TestCase_launch.test_failurecode.
       
   265             raise LaunchError(1, cmd, stdout + " " + stderr + ": " + str(inst))
       
   266 
       
   267         if p.returncode == 0:
       
   268             if split_lines:
       
   269                 # Setting keepends=True for compatibility with previous logic
       
   270                 # (where file.readlines() preserves newlines)
       
   271                 return stdout.splitlines(True)
       
   272             else:
       
   273                 return stdout
       
   274         else:
       
   275             raise LaunchError(p.returncode, cmd, stdout + stderr)
       
   276 except ImportError:
       
   277     # support versions of python before 2.4 (slower on some systems)
       
   278     def launch(cmd, split_lines=True):
       
   279         if os.name not in ['nt', 'os2']:
       
   280             p = popen2.Popen4(cmd)
       
   281             p.tochild.close()
       
   282             if split_lines:
       
   283                 out = p.fromchild.readlines()
       
   284             else:
       
   285                 out = p.fromchild.read()
       
   286             ret = p.wait()
       
   287             if ret == 0:
       
   288                 ret = None
       
   289             else:
       
   290                 ret >>= 8
       
   291         else:
       
   292             i,k = os.popen4(cmd)
       
   293             i.close()
       
   294             if split_lines:
       
   295                 out = k.readlines()
       
   296             else:
       
   297                 out = k.read()
       
   298             ret = k.close()
       
   299 
       
   300         if ret is None:
       
   301             return out
       
   302         raise LaunchError(ret, cmd, out)
       
   303 
       
   304 def launchsvn(s, show=False, pretend=False, **kwargs):
       
   305     """Launch SVN and grab its output."""
       
   306     username = opts.get("username", None)
       
   307     password = opts.get("password", None)
       
   308     if username:
       
   309         username = " --username=" + username
       
   310     else:
       
   311         username = ""
       
   312     if password:
       
   313         password = " --password=" + password
       
   314     else:
       
   315         password = ""
       
   316     cmd = opts["svn"] + " --non-interactive" + username + password + " " + s
       
   317     if show or opts["verbose"] >= 2:
       
   318         print cmd
       
   319     if pretend:
       
   320         return None
       
   321     return launch(cmd, **kwargs)
       
   322 
       
   323 def svn_command(s):
       
   324     """Do (or pretend to do) an SVN command."""
       
   325     out = launchsvn(s, show=opts["show-changes"] or opts["dry-run"],
       
   326                     pretend=opts["dry-run"],
       
   327                     split_lines=False)
       
   328     if not opts["dry-run"]:
       
   329         print out
       
   330 
       
   331 def check_dir_clean(dir):
       
   332     """Check the current status of dir for local mods."""
       
   333     if opts["force"]:
       
   334         report('skipping status check because of --force')
       
   335         return
       
   336     report('checking status of "%s"' % dir)
       
   337 
       
   338     # Checking with -q does not show unversioned files or external
       
   339     # directories.  Though it displays a debug message for external
       
   340     # directories, after a blank line.  So, practically, the first line
       
   341     # matters: if it's non-empty there is a modification.
       
   342     out = launchsvn("status -q %s" % dir)
       
   343     if out and out[0].strip():
       
   344         error('"%s" has local modifications; it must be clean' % dir)
       
   345 
       
   346 class RevisionLog:
       
   347     """
       
   348     A log of the revisions which affected a given URL between two
       
   349     revisions.
       
   350     """
       
   351 
       
   352     def __init__(self, url, begin, end, find_propchanges=False):
       
   353         """
       
   354         Create a new RevisionLog object, which stores, in self.revs, a list
       
   355         of the revisions which affected the specified URL between begin and
       
   356         end. If find_propchanges is True, self.propchange_revs will contain a
       
   357         list of the revisions which changed properties directly on the
       
   358         specified URL. URL must be the URL for a directory in the repository.
       
   359         """
       
   360         self.url = url
       
   361 
       
   362         # Setup the log options (--quiet, so we don't show log messages)
       
   363         log_opts = '--xml --quiet -r%s:%s "%s"' % (begin, end, url)
       
   364         if find_propchanges:
       
   365             # The --verbose flag lets us grab merge tracking information
       
   366             # by looking at propchanges
       
   367             log_opts = "--verbose " + log_opts
       
   368 
       
   369         # Read the log to look for revision numbers and merge-tracking info
       
   370         self.revs = []
       
   371         self.propchange_revs = []
       
   372         repos_pathid = target_to_pathid(url)
       
   373         for chg in SvnLogParser(launchsvn("log %s" % log_opts,
       
   374                                           split_lines=False)):
       
   375             self.revs.append(chg.revision())
       
   376             for p in chg.paths():
       
   377                 if p.action() == 'M' and p.pathid() == repos_pathid:
       
   378                     self.propchange_revs.append(chg.revision())
       
   379 
       
   380         # Save the range of the log
       
   381         self.begin = int(begin)
       
   382         if end == "HEAD":
       
   383             # If end is not provided, we do not know which is the latest
       
   384             # revision in the repository. So we set 'end' to the latest
       
   385             # known revision.
       
   386             self.end = self.revs[-1]
       
   387         else:
       
   388             self.end = int(end)
       
   389 
       
   390         self._merges = None
       
   391         self._blocks = None
       
   392 
       
   393     def merge_metadata(self):
       
   394         """
       
   395         Return a VersionedProperty object, with a cached view of the merge
       
   396         metadata in the range of this log.
       
   397         """
       
   398 
       
   399         # Load merge metadata if necessary
       
   400         if not self._merges:
       
   401             self._merges = VersionedProperty(self.url, opts["prop"])
       
   402             self._merges.load(self)
       
   403 
       
   404         return self._merges
       
   405 
       
   406     def block_metadata(self):
       
   407         if not self._blocks:
       
   408             self._blocks = VersionedProperty(self.url, opts["block-prop"])
       
   409             self._blocks.load(self)
       
   410 
       
   411         return self._blocks
       
   412 
       
   413 
       
   414 class VersionedProperty:
       
   415     """
       
   416     A read-only, cached view of a versioned property.
       
   417 
       
   418     self.revs contains a list of the revisions in which the property changes.
       
   419     self.values stores the new values at each corresponding revision. If the
       
   420     value of the property is unknown, it is set to None.
       
   421 
       
   422     Initially, we set self.revs to [0] and self.values to [None]. This
       
   423     indicates that, as of revision zero, we know nothing about the value of
       
   424     the property.
       
   425 
       
   426     Later, if you run self.load(log), we cache the value of this property over
       
   427     the entire range of the log by noting each revision in which the property
       
   428     was changed. At the end of the range of the log, we invalidate our cache
       
   429     by adding the value "None" to our cache for any revisions which fall out
       
   430     of the range of our log.
       
   431 
       
   432     Once self.revs and self.values are filled, we can find the value of the
       
   433     property at any arbitrary revision using a binary search on self.revs.
       
   434     Once we find the last revision during which the property was changed,
       
   435     we can lookup the associated value in self.values. (If the associated
       
   436     value is None, the associated value was not cached and we have to do
       
   437     a full propget.)
       
   438 
       
   439     An example: We know that the 'svnmerge' property was added in r10, and
       
   440     changed in r21. We gathered log info up until r40.
       
   441 
       
   442     revs = [0, 10, 21, 40]
       
   443     values = [None, "val1", "val2", None]
       
   444 
       
   445     What these values say:
       
   446     - From r0 to r9, we know nothing about the property.
       
   447     - In r10, the property was set to "val1". This property stayed the same
       
   448       until r21, when it was changed to "val2".
       
   449     - We don't know what happened after r40.
       
   450     """
       
   451 
       
   452     def __init__(self, url, name):
       
   453         """View the history of a versioned property at URL with name"""
       
   454         self.url = url
       
   455         self.name = name
       
   456 
       
   457         # We know nothing about the value of the property. Setup revs
       
   458         # and values to indicate as such.
       
   459         self.revs = [0]
       
   460         self.values = [None]
       
   461 
       
   462         # We don't have any revisions cached
       
   463         self._initial_value = None
       
   464         self._changed_revs = []
       
   465         self._changed_values = []
       
   466 
       
   467     def load(self, log):
       
   468         """
       
   469         Load the history of property changes from the specified
       
   470         RevisionLog object.
       
   471         """
       
   472 
       
   473         # Get the property value before the range of the log
       
   474         if log.begin > 1:
       
   475             self.revs.append(log.begin-1)
       
   476             try:
       
   477                 self._initial_value = self.raw_get(log.begin-1)
       
   478             except LaunchError:
       
   479                 # The specified URL might not exist before the
       
   480                 # range of the log. If so, we can safely assume
       
   481                 # that the property was empty at that time.
       
   482                 self._initial_value = { }
       
   483             self.values.append(self._initial_value)
       
   484         else:
       
   485             self._initial_value = { }
       
   486             self.values[0] = self._initial_value
       
   487 
       
   488         # Cache the property values in the log range
       
   489         old_value = self._initial_value
       
   490         for rev in log.propchange_revs:
       
   491             new_value = self.raw_get(rev)
       
   492             if new_value != old_value:
       
   493                 self._changed_revs.append(rev)
       
   494                 self._changed_values.append(new_value)
       
   495                 self.revs.append(rev)
       
   496                 self.values.append(new_value)
       
   497                 old_value = new_value
       
   498 
       
   499         # Indicate that we know nothing about the value of the property
       
   500         # after the range of the log.
       
   501         if log.revs:
       
   502             self.revs.append(log.end+1)
       
   503             self.values.append(None)
       
   504 
       
   505     def raw_get(self, rev=None):
       
   506         """
       
   507         Get the property at revision REV. If rev is not specified, get
       
   508         the property at revision HEAD.
       
   509         """
       
   510         return get_revlist_prop(self.url, self.name, rev)
       
   511 
       
   512     def get(self, rev=None):
       
   513         """
       
   514         Get the property at revision REV. If rev is not specified, get
       
   515         the property at revision HEAD.
       
   516         """
       
   517 
       
   518         if rev is not None:
       
   519 
       
   520             # Find the index using a binary search
       
   521             i = bisect(self.revs, rev) - 1
       
   522 
       
   523             # Return the value of the property, if it was cached
       
   524             if self.values[i] is not None:
       
   525                 return self.values[i]
       
   526 
       
   527         # Get the current value of the property
       
   528         return self.raw_get(rev)
       
   529 
       
   530     def changed_revs(self, key=None):
       
   531         """
       
   532         Get a list of the revisions in which the specified dictionary
       
   533         key was changed in this property. If key is not specified,
       
   534         return a list of revisions in which any key was changed.
       
   535         """
       
   536         if key is None:
       
   537             return self._changed_revs
       
   538         else:
       
   539             changed_revs = []
       
   540             old_val = self._initial_value
       
   541             for rev, val in zip(self._changed_revs, self._changed_values):
       
   542                 if val.get(key) != old_val.get(key):
       
   543                     changed_revs.append(rev)
       
   544                     old_val = val
       
   545             return changed_revs
       
   546 
       
   547     def initialized_revs(self):
       
   548         """
       
   549         Get a list of the revisions in which keys were added or
       
   550         removed in this property.
       
   551         """
       
   552         initialized_revs = []
       
   553         old_len = len(self._initial_value)
       
   554         for rev, val in zip(self._changed_revs, self._changed_values):
       
   555             if len(val) != old_len:
       
   556                 initialized_revs.append(rev)
       
   557                 old_len = len(val)
       
   558         return initialized_revs
       
   559 
       
   560 class RevisionSet:
       
   561     """
       
   562     A set of revisions, held in dictionary form for easy manipulation. If we
       
   563     were to rewrite this script for Python 2.3+, we would subclass this from
       
   564     set (or UserSet).  As this class does not include branch
       
   565     information, it's assumed that one instance will be used per
       
   566     branch.
       
   567     """
       
   568     def __init__(self, parm):
       
   569         """Constructs a RevisionSet from a string in property form, or from
       
   570         a dictionary whose keys are the revisions. Raises ValueError if the
       
   571         input string is invalid."""
       
   572 
       
   573         self._revs = {}
       
   574 
       
   575         revision_range_split_re = re.compile('[-:]')
       
   576 
       
   577         if isinstance(parm, types.DictType):
       
   578             self._revs = parm.copy()
       
   579         elif isinstance(parm, types.ListType):
       
   580             for R in parm:
       
   581                 self._revs[int(R)] = 1
       
   582         else:
       
   583             parm = parm.strip()
       
   584             if parm:
       
   585                 for R in parm.split(","):
       
   586                     rev_or_revs = re.split(revision_range_split_re, R)
       
   587                     if len(rev_or_revs) == 1:
       
   588                         self._revs[int(rev_or_revs[0])] = 1
       
   589                     elif len(rev_or_revs) == 2:
       
   590                         for rev in range(int(rev_or_revs[0]),
       
   591                                          int(rev_or_revs[1])+1):
       
   592                             self._revs[rev] = 1
       
   593                     else:
       
   594                         raise ValueError, 'Ill formatted revision range: ' + R
       
   595 
       
   596     def sorted(self):
       
   597         revnums = self._revs.keys()
       
   598         revnums.sort()
       
   599         return revnums
       
   600 
       
   601     def normalized(self):
       
   602         """Returns a normalized version of the revision set, which is an
       
   603         ordered list of couples (start,end), with the minimum number of
       
   604         intervals."""
       
   605         revnums = self.sorted()
       
   606         revnums.reverse()
       
   607         ret = []
       
   608         while revnums:
       
   609             s = e = revnums.pop()
       
   610             while revnums and revnums[-1] in (e, e+1):
       
   611                 e = revnums.pop()
       
   612             ret.append((s, e))
       
   613         return ret
       
   614 
       
   615     def __str__(self):
       
   616         """Convert the revision set to a string, using its normalized form."""
       
   617         L = []
       
   618         for s,e in self.normalized():
       
   619             if s == e:
       
   620                 L.append(str(s))
       
   621             else:
       
   622                 L.append(str(s) + "-" + str(e))
       
   623         return ",".join(L)
       
   624 
       
   625     def __contains__(self, rev):
       
   626         return self._revs.has_key(rev)
       
   627 
       
   628     def __sub__(self, rs):
       
   629         """Compute subtraction as in sets."""
       
   630         revs = {}
       
   631         for r in self._revs.keys():
       
   632             if r not in rs:
       
   633                 revs[r] = 1
       
   634         return RevisionSet(revs)
       
   635 
       
   636     def __and__(self, rs):
       
   637         """Compute intersections as in sets."""
       
   638         revs = {}
       
   639         for r in self._revs.keys():
       
   640             if r in rs:
       
   641                 revs[r] = 1
       
   642         return RevisionSet(revs)
       
   643 
       
   644     def __nonzero__(self):
       
   645         return len(self._revs) != 0
       
   646 
       
   647     def __len__(self):
       
   648         """Return the number of revisions in the set."""
       
   649         return len(self._revs)
       
   650 
       
   651     def __iter__(self):
       
   652         return iter(self.sorted())
       
   653 
       
   654     def __or__(self, rs):
       
   655         """Compute set union."""
       
   656         revs = self._revs.copy()
       
   657         revs.update(rs._revs)
       
   658         return RevisionSet(revs)
       
   659 
       
   660 def merge_props_to_revision_set(merge_props, pathid):
       
   661     """A converter which returns a RevisionSet instance containing the
       
   662     revisions from PATH as known to BRANCH_PROPS.  BRANCH_PROPS is a
       
   663     dictionary of pathid -> revision set branch integration information
       
   664     (as returned by get_merge_props())."""
       
   665     if not merge_props.has_key(pathid):
       
   666         error('no integration info available for path "%s"' % pathid)
       
   667     return RevisionSet(merge_props[pathid])
       
   668 
       
   669 def dict_from_revlist_prop(propvalue):
       
   670     """Given a property value as a string containing per-source revision
       
   671     lists, return a dictionary whose key is a source path identifier
       
   672     and whose value is the revisions for that source."""
       
   673     prop = {}
       
   674 
       
   675     # Multiple sources are separated by any whitespace.
       
   676     for L in propvalue.split():
       
   677         # We use rsplit to play safe and allow colons in pathids.
       
   678         source, revs = rsplit(L.strip(), ":", 1)
       
   679         prop[source] = revs
       
   680     return prop
       
   681 
       
   682 def get_revlist_prop(url_or_dir, propname, rev=None):
       
   683     """Given a repository URL or working copy path and a property
       
   684     name, extract the values of the property which store per-source
       
   685     revision lists and return a dictionary whose key is a source path
       
   686     identifier, and whose value is the revisions for that source."""
       
   687 
       
   688     # Note that propget does not return an error if the property does
       
   689     # not exist, it simply does not output anything. So we do not need
       
   690     # to check for LaunchError here.
       
   691     args = '--strict "%s" "%s"' % (propname, url_or_dir)
       
   692     if rev:
       
   693         args = '-r %s %s' % (rev, args)
       
   694     out = launchsvn('propget %s' % args, split_lines=False)
       
   695 
       
   696     return dict_from_revlist_prop(out)
       
   697 
       
   698 def get_merge_props(dir):
       
   699     """Extract the merged revisions."""
       
   700     return get_revlist_prop(dir, opts["prop"])
       
   701 
       
   702 def get_block_props(dir):
       
   703     """Extract the blocked revisions."""
       
   704     return get_revlist_prop(dir, opts["block-prop"])
       
   705 
       
   706 def get_blocked_revs(dir, source_pathid):
       
   707     p = get_block_props(dir)
       
   708     if p.has_key(source_pathid):
       
   709         return RevisionSet(p[source_pathid])
       
   710     return RevisionSet("")
       
   711 
       
   712 def format_merge_props(props, sep=" "):
       
   713     """Formats the hash PROPS as a string suitable for use as a
       
   714     Subversion property value."""
       
   715     assert sep in ["\t", "\n", " "]   # must be a whitespace
       
   716     props = props.items()
       
   717     props.sort()
       
   718     L = []
       
   719     for h, r in props:
       
   720         L.append(h + ":" + r)
       
   721     return sep.join(L)
       
   722 
       
   723 def _run_propset(dir, prop, value):
       
   724     """Set the property 'prop' of directory 'dir' to value 'value'. We go
       
   725     through a temporary file to not run into command line length limits."""
       
   726     try:
       
   727         fd, fname = tempfile.mkstemp()
       
   728         f = os.fdopen(fd, "wb")
       
   729     except AttributeError:
       
   730         # Fallback for Python <= 2.3 which does not have mkstemp (mktemp
       
   731         # suffers from race conditions. Not that we care...)
       
   732         fname = tempfile.mktemp()
       
   733         f = open(fname, "wb")
       
   734 
       
   735     try:
       
   736         f.write(value)
       
   737         f.close()
       
   738         report("property data written to temp file: %s" % value)
       
   739         svn_command('propset "%s" -F "%s" "%s"' % (prop, fname, dir))
       
   740     finally:
       
   741         os.remove(fname)
       
   742 
       
   743 def set_props(dir, name, props):
       
   744     props = format_merge_props(props)
       
   745     if props:
       
   746         _run_propset(dir, name, props)
       
   747     else:
       
   748         svn_command('propdel "%s" "%s"' % (name, dir))
       
   749 
       
   750 def set_merge_props(dir, props):
       
   751     set_props(dir, opts["prop"], props)
       
   752 
       
   753 def set_block_props(dir, props):
       
   754     set_props(dir, opts["block-prop"], props)
       
   755 
       
   756 def set_blocked_revs(dir, source_pathid, revs):
       
   757     props = get_block_props(dir)
       
   758     if revs:
       
   759         props[source_pathid] = str(revs)
       
   760     elif props.has_key(source_pathid):
       
   761         del props[source_pathid]
       
   762     set_block_props(dir, props)
       
   763 
       
   764 def is_url(url):
       
   765     """Check if url is a valid url."""
       
   766     return re.search(r"^[a-zA-Z][-+\.\w]*://[^\s]+$", url) is not None
       
   767 
       
   768 def is_wc(dir):
       
   769     """Check if a directory is a working copy."""
       
   770     return os.path.isdir(os.path.join(dir, ".svn")) or \
       
   771            os.path.isdir(os.path.join(dir, "_svn"))
       
   772 
       
   773 _cache_svninfo = {}
       
   774 def get_svninfo(target):
       
   775     """Extract the subversion information for a target (through 'svn info').
       
   776     This function uses an internal cache to let clients query information
       
   777     many times."""
       
   778     if _cache_svninfo.has_key(target):
       
   779         return _cache_svninfo[target]
       
   780     info = {}
       
   781     for L in launchsvn('info "%s"' % target):
       
   782         L = L.strip()
       
   783         if not L:
       
   784             continue
       
   785         key, value = L.split(": ", 1)
       
   786         info[key] = value.strip()
       
   787     _cache_svninfo[target] = info
       
   788     return info
       
   789 
       
   790 def target_to_url(target):
       
   791     """Convert working copy path or repos URL to a repos URL."""
       
   792     if is_wc(target):
       
   793         info = get_svninfo(target)
       
   794         return info["URL"]
       
   795     return target
       
   796 
       
   797 _cache_reporoot = {}
       
   798 def get_repo_root(target):
       
   799     """Compute the root repos URL given a working-copy path, or a URL."""
       
   800     # Try using "svn info WCDIR". This works only on SVN clients >= 1.3
       
   801     if not is_url(target):
       
   802         try:
       
   803             info = get_svninfo(target)
       
   804             root = info["Repository Root"]
       
   805             _cache_reporoot[root] = None
       
   806             return root
       
   807         except KeyError:
       
   808             pass
       
   809         url = target_to_url(target)
       
   810         assert url[-1] != '/'
       
   811     else:
       
   812         url = target
       
   813 
       
   814     # Go through the cache of the repository roots. This avoids extra
       
   815     # server round-trips if we are asking the root of different URLs
       
   816     # in the same repository (the cache in get_svninfo() cannot detect
       
   817     # that of course and would issue a remote command).
       
   818     assert is_url(url)
       
   819     for r in _cache_reporoot:
       
   820         if url.startswith(r):
       
   821             return r
       
   822 
       
   823     # Try using "svn info URL". This works only on SVN clients >= 1.2
       
   824     try:
       
   825         info = get_svninfo(url)
       
   826         root = info["Repository Root"]
       
   827         _cache_reporoot[root] = None
       
   828         return root
       
   829     except LaunchError:
       
   830         pass
       
   831 
       
   832     # Constrained to older svn clients, we are stuck with this ugly
       
   833     # trial-and-error implementation. It could be made faster with a
       
   834     # binary search.
       
   835     while url:
       
   836         temp = os.path.dirname(url)
       
   837         try:
       
   838             launchsvn('proplist "%s"' % temp)
       
   839         except LaunchError:
       
   840             _cache_reporoot[url] = None
       
   841             return url
       
   842         url = temp
       
   843 
       
   844     assert False, "svn repos root not found"
       
   845 
       
   846 def target_to_pathid(target):
       
   847     """Convert a target (either a working copy path or an URL) into a
       
   848     path identifier."""
       
   849     root = get_repo_root(target)
       
   850     url = target_to_url(target)
       
   851     assert root[-1] != "/"
       
   852     assert url[:len(root)] == root, "url=%r, root=%r" % (url, root)
       
   853     return url[len(root):]
       
   854 
       
   855 class SvnLogParser:
       
   856     """
       
   857     Parse the "svn log", going through the XML output and using pulldom (which
       
   858     would even allow streaming the command output).
       
   859     """
       
   860     def __init__(self, xml):
       
   861         self._events = pulldom.parseString(xml)
       
   862     def __getitem__(self, idx):
       
   863         for event, node in self._events:
       
   864             if event == pulldom.START_ELEMENT and node.tagName == "logentry":
       
   865                 self._events.expandNode(node)
       
   866                 return self.SvnLogRevision(node)
       
   867         raise IndexError, "Could not find 'logentry' tag in xml"
       
   868 
       
   869     class SvnLogRevision:
       
   870         def __init__(self, xmlnode):
       
   871             self.n = xmlnode
       
   872         def revision(self):
       
   873             return int(self.n.getAttribute("revision"))
       
   874         def author(self):
       
   875             return self.n.getElementsByTagName("author")[0].firstChild.data
       
   876         def paths(self):
       
   877             return [self.SvnLogPath(n)
       
   878                     for n in  self.n.getElementsByTagName("path")]
       
   879 
       
   880         class SvnLogPath:
       
   881             def __init__(self, xmlnode):
       
   882                 self.n = xmlnode
       
   883             def action(self):
       
   884                 return self.n.getAttribute("action")
       
   885             def pathid(self):
       
   886                 return self.n.firstChild.data
       
   887             def copyfrom_rev(self):
       
   888                 try: return self.n.getAttribute("copyfrom-rev")
       
   889                 except KeyError: return None
       
   890             def copyfrom_pathid(self):
       
   891                 try: return self.n.getAttribute("copyfrom-path")
       
   892                 except KeyError: return None
       
   893 
       
   894 def get_copyfrom(target):
       
   895     """Get copyfrom info for a given target (it represents the directory from
       
   896     where it was branched). NOTE: repos root has no copyfrom info. In this case
       
   897     None is returned.
       
   898 
       
   899     Returns the:
       
   900         - source file or directory from which the copy was made
       
   901         - revision from which that source was copied
       
   902         - revision in which the copy was committed
       
   903     """
       
   904     repos_path = target_to_pathid(target)
       
   905     for chg in SvnLogParser(launchsvn('log -v --xml --stop-on-copy "%s"'
       
   906                                       % target, split_lines=False)):
       
   907         for p in chg.paths():
       
   908             if p.action() == 'A' and p.pathid() == repos_path:
       
   909                 # These values will be None if the corresponding elements are
       
   910                 # not found in the log.
       
   911                 return p.copyfrom_pathid(), p.copyfrom_rev(), chg.revision()
       
   912     return None,None,None
       
   913 
       
   914 def get_latest_rev(url):
       
   915     """Get the latest revision of the repository of which URL is part."""
       
   916     try:
       
   917         return get_svninfo(url)["Revision"]
       
   918     except LaunchError:
       
   919         # Alternative method for latest revision checking (for svn < 1.2)
       
   920         report('checking latest revision of "%s"' % url)
       
   921         L = launchsvn('proplist --revprop -r HEAD "%s"' % opts["source-url"])[0]
       
   922         rev = re.search("revision (\d+)", L).group(1)
       
   923         report('latest revision of "%s" is %s' % (url, rev))
       
   924         return rev
       
   925 
       
   926 def get_created_rev(url):
       
   927     """Lookup the revision at which the path identified by the
       
   928     provided URL was first created."""
       
   929     oldest_rev = -1
       
   930     report('determining oldest revision for URL "%s"' % url)
       
   931     ### TODO: Refactor this to use a modified RevisionLog class.
       
   932     lines = None
       
   933     cmd = "log -r1:HEAD --stop-on-copy -q " + url
       
   934     try:
       
   935         lines = launchsvn(cmd + " --limit=1")
       
   936     except LaunchError:
       
   937         # Assume that --limit isn't supported by the installed 'svn'.
       
   938         lines = launchsvn(cmd)
       
   939     if lines and len(lines) > 1:
       
   940         i = lines[1].find(" ")
       
   941         if i != -1:
       
   942             oldest_rev = int(lines[1][1:i])
       
   943     if oldest_rev == -1:
       
   944         error('unable to determine oldest revision for URL "%s"' % url)
       
   945     return oldest_rev
       
   946 
       
   947 def get_commit_log(url, revnum):
       
   948     """Return the log message for a specific integer revision
       
   949     number."""
       
   950     out = launchsvn("log --incremental -r%d %s" % (revnum, url))
       
   951     return recode_stdout_to_file("".join(out[1:]))
       
   952 
       
   953 def construct_merged_log_message(url, revnums):
       
   954     """Return a commit log message containing all the commit messages
       
   955     in the specified revisions at the given URL.  The separator used
       
   956     in this log message is determined by searching for the longest
       
   957     svnmerge separator existing in the commit log messages and
       
   958     extending it by one more separator.  This results in a new commit
       
   959     log message that is clearer in describing merges that contain
       
   960     other merges. Trailing newlines are removed from the embedded
       
   961     log messages."""
       
   962     messages = ['']
       
   963     longest_sep = ''
       
   964     for r in revnums.sorted():
       
   965         message = get_commit_log(url, r)
       
   966         if message:
       
   967             message = re.sub(r'(\r\n|\r|\n)', "\n", message)
       
   968             message = rstrip(message, "\n") + "\n"
       
   969             messages.append(prefix_lines(LOG_LINE_PREFIX, message))
       
   970             for match in LOG_SEPARATOR_RE.findall(message):
       
   971                 sep = match[1]
       
   972                 if len(sep) > len(longest_sep):
       
   973                     longest_sep = sep
       
   974 
       
   975     longest_sep += LOG_SEPARATOR + "\n"
       
   976     messages.append('')
       
   977     return longest_sep.join(messages)
       
   978 
       
   979 def get_default_source(branch_target, branch_props):
       
   980     """Return the default source for branch_target (given its branch_props).
       
   981     Error out if there is ambiguity."""
       
   982     if not branch_props:
       
   983         error("no integration info available")
       
   984 
       
   985     props = branch_props.copy()
       
   986     pathid = target_to_pathid(branch_target)
       
   987 
       
   988     # To make bidirectional merges easier, find the target's
       
   989     # repository local path so it can be removed from the list of
       
   990     # possible integration sources.
       
   991     if props.has_key(pathid):
       
   992         del props[pathid]
       
   993 
       
   994     if len(props) > 1:
       
   995         err_msg = "multiple sources found. "
       
   996         err_msg += "Explicit source argument (-S/--source) required.\n"
       
   997         err_msg += "The merge sources available are:"
       
   998         for prop in props:
       
   999           err_msg += "\n  " + prop
       
  1000         error(err_msg)
       
  1001 
       
  1002     return props.keys()[0]
       
  1003 
       
  1004 def check_old_prop_version(branch_target, branch_props):
       
  1005     """Check if branch_props (of branch_target) are svnmerge properties in
       
  1006     old format, and emit an error if so."""
       
  1007 
       
  1008     # Previous svnmerge versions allowed trailing /'s in the repository
       
  1009     # local path.  Newer versions of svnmerge will trim trailing /'s
       
  1010     # appearing in the command line, so if there are any properties with
       
  1011     # trailing /'s, they will not be properly matched later on, so require
       
  1012     # the user to change them now.
       
  1013     fixed = {}
       
  1014     changed = False
       
  1015     for source, revs in branch_props.items():
       
  1016         src = rstrip(source, "/")
       
  1017         fixed[src] = revs
       
  1018         if src != source:
       
  1019             changed = True
       
  1020 
       
  1021     if changed:
       
  1022         err_msg = "old property values detected; an upgrade is required.\n\n"
       
  1023         err_msg += "Please execute and commit these changes to upgrade:\n\n"
       
  1024         err_msg += 'svn propset "%s" "%s" "%s"' % \
       
  1025                    (opts["prop"], format_merge_props(fixed), branch_target)
       
  1026         error(err_msg)
       
  1027 
       
  1028 def should_find_reflected(branch_dir):
       
  1029     should_find_reflected = opts["bidirectional"]
       
  1030 
       
  1031     # If the source has integration info for the target, set find_reflected
       
  1032     # even if --bidirectional wasn't specified
       
  1033     if not should_find_reflected:
       
  1034         source_props = get_merge_props(opts["source-url"])
       
  1035         should_find_reflected = source_props.has_key(target_to_pathid(branch_dir))
       
  1036 
       
  1037     return should_find_reflected
       
  1038 
       
  1039 def analyze_revs(target_pathid, url, begin=1, end=None,
       
  1040                  find_reflected=False):
       
  1041     """For the source of the merges in the source URL being merged into
       
  1042     target_pathid, analyze the revisions in the interval begin-end (which
       
  1043     defaults to 1-HEAD), to find out which revisions are changes in
       
  1044     the url, which are changes elsewhere (so-called 'phantom'
       
  1045     revisions), optionally which are reflected changes (to avoid
       
  1046     conflicts that can occur when doing bidirectional merging between
       
  1047     branches), and which revisions initialize merge tracking against other
       
  1048     branches.  Return a tuple of four RevisionSet's:
       
  1049         (real_revs, phantom_revs, reflected_revs, initialized_revs).
       
  1050 
       
  1051     NOTE: To maximize speed, if "end" is not provided, the function is
       
  1052     not able to find phantom revisions following the last real
       
  1053     revision in the URL.
       
  1054     """
       
  1055 
       
  1056     begin = str(begin)
       
  1057     if end is None:
       
  1058         end = "HEAD"
       
  1059     else:
       
  1060         end = str(end)
       
  1061         if long(begin) > long(end):
       
  1062             return RevisionSet(""), RevisionSet(""), \
       
  1063                    RevisionSet(""), RevisionSet("")
       
  1064 
       
  1065     logs[url] = RevisionLog(url, begin, end, find_reflected)
       
  1066     revs = RevisionSet(logs[url].revs)
       
  1067 
       
  1068     if end == "HEAD":
       
  1069         # If end is not provided, we do not know which is the latest revision
       
  1070         # in the repository. So return the phantom revision set only up to
       
  1071         # the latest known revision.
       
  1072         end = str(list(revs)[-1])
       
  1073 
       
  1074     phantom_revs = RevisionSet("%s-%s" % (begin, end)) - revs
       
  1075 
       
  1076     if find_reflected:
       
  1077         reflected_revs = logs[url].merge_metadata().changed_revs(target_pathid)
       
  1078         reflected_revs += logs[url].block_metadata().changed_revs(target_pathid)
       
  1079     else:
       
  1080         reflected_revs = []
       
  1081 
       
  1082     initialized_revs = RevisionSet(logs[url].merge_metadata().initialized_revs())
       
  1083     reflected_revs = RevisionSet(reflected_revs)
       
  1084 
       
  1085     return revs, phantom_revs, reflected_revs, initialized_revs
       
  1086 
       
  1087 def analyze_source_revs(branch_target, source_url, **kwargs):
       
  1088     """For the given branch and source, extract the real and phantom
       
  1089     source revisions."""
       
  1090     branch_url = target_to_url(branch_target)
       
  1091     branch_pathid = target_to_pathid(branch_target)
       
  1092 
       
  1093     # Extract the latest repository revision from the URL of the branch
       
  1094     # directory (which is already cached at this point).
       
  1095     end_rev = get_latest_rev(source_url)
       
  1096 
       
  1097     # Calculate the base of analysis. If there is a "1-XX" interval in the
       
  1098     # merged_revs, we do not need to check those.
       
  1099     base = 1
       
  1100     r = opts["merged-revs"].normalized()
       
  1101     if r and r[0][0] == 1:
       
  1102         base = r[0][1] + 1
       
  1103 
       
  1104     # See if the user filtered the revision set. If so, we are not
       
  1105     # interested in something outside that range.
       
  1106     if opts["revision"]:
       
  1107         revs = RevisionSet(opts["revision"]).sorted()
       
  1108         if base < revs[0]:
       
  1109             base = revs[0]
       
  1110         if end_rev > revs[-1]:
       
  1111             end_rev = revs[-1]
       
  1112 
       
  1113     return analyze_revs(branch_pathid, source_url, base, end_rev, **kwargs)
       
  1114 
       
  1115 def minimal_merge_intervals(revs, phantom_revs):
       
  1116     """Produce the smallest number of intervals suitable for merging. revs
       
  1117     is the RevisionSet which we want to merge, and phantom_revs are phantom
       
  1118     revisions which can be used to concatenate intervals, thus minimizing the
       
  1119     number of operations."""
       
  1120     revnums = revs.normalized()
       
  1121     ret = []
       
  1122 
       
  1123     cur = revnums.pop()
       
  1124     while revnums:
       
  1125         next = revnums.pop()
       
  1126         assert next[1] < cur[0]      # otherwise it is not ordered
       
  1127         assert cur[0] - next[1] > 1  # otherwise it is not normalized
       
  1128         for i in range(next[1]+1, cur[0]):
       
  1129             if i not in phantom_revs:
       
  1130                 ret.append(cur)
       
  1131                 cur = next
       
  1132                 break
       
  1133         else:
       
  1134             cur = (next[0], cur[1])
       
  1135 
       
  1136     ret.append(cur)
       
  1137     ret.reverse()
       
  1138     return ret
       
  1139 
       
  1140 def display_revisions(revs, display_style, revisions_msg, source_url):
       
  1141     """Show REVS as dictated by DISPLAY_STYLE, either numerically, in
       
  1142     log format, or as diffs.  When displaying revisions numerically,
       
  1143     prefix output with REVISIONS_MSG when in verbose mode.  Otherwise,
       
  1144     request logs or diffs using SOURCE_URL."""
       
  1145     if display_style == "revisions":
       
  1146         if revs:
       
  1147             report(revisions_msg)
       
  1148             print revs
       
  1149     elif display_style == "logs":
       
  1150         for start,end in revs.normalized():
       
  1151             svn_command('log --incremental -v -r %d:%d %s' % \
       
  1152                         (start, end, source_url))
       
  1153     elif display_style in ("diffs", "summarize"):
       
  1154         if display_style == 'summarize':
       
  1155             summarize = '--summarize '
       
  1156         else:
       
  1157             summarize = ''
       
  1158 
       
  1159         for start, end in revs.normalized():
       
  1160             print
       
  1161             if start == end:
       
  1162                 print "%s: changes in revision %d follow" % (NAME, start)
       
  1163             else:
       
  1164                 print "%s: changes in revisions %d-%d follow" % (NAME,
       
  1165                                                                  start, end)
       
  1166             print
       
  1167 
       
  1168             # Note: the starting revision number to 'svn diff' is
       
  1169             # NOT inclusive so we have to subtract one from ${START}.
       
  1170             svn_command("diff -r %d:%d %s %s" % (start - 1, end, summarize,
       
  1171                                                  source_url))
       
  1172     else:
       
  1173         assert False, "unhandled display style: %s" % display_style
       
  1174 
       
  1175 def action_init(target_dir, target_props):
       
  1176     """Initialize for merges."""
       
  1177     # Check that directory is ready for being modified
       
  1178     check_dir_clean(target_dir)
       
  1179 
       
  1180     # If the user hasn't specified the revisions to use, see if the
       
  1181     # "source" is a copy from the current tree and if so, we can use
       
  1182     # the version data obtained from it.
       
  1183     revision_range = opts["revision"]
       
  1184     if not revision_range:
       
  1185         # Determining a default endpoint for the revision range that "init"
       
  1186         # will use, since none was provided by the user.
       
  1187         cf_source, cf_rev, copy_committed_in_rev = \
       
  1188                                             get_copyfrom(opts["source-url"])
       
  1189         target_path = target_to_pathid(target_dir)
       
  1190 
       
  1191         if target_path == cf_source:
       
  1192             # If source was originally copyied from target, and we are merging
       
  1193             # changes from source to target (the copy target is the merge
       
  1194             # source, and the copy source is the merge target), then we want to
       
  1195             # mark as integrated up to the rev in which the copy was committed
       
  1196             # which created the merge source:
       
  1197             report('the source "%s" is a branch of "%s"' %
       
  1198                    (opts["source-url"], target_dir))
       
  1199             revision_range = "1-" + str(copy_committed_in_rev)
       
  1200         else:
       
  1201             # If the copy source is the merge source, and
       
  1202             # the copy target is the merge target, then we want to
       
  1203             # mark as integrated up to the specific rev of the merge
       
  1204             # target from which the merge source was copied. Longer
       
  1205             # discussion here:
       
  1206             # http://subversion.tigris.org/issues/show_bug.cgi?id=2810
       
  1207             target_url = target_to_url(target_dir)
       
  1208             source_path = target_to_pathid(opts["source-url"])
       
  1209             cf_source_path, cf_rev, copy_committed_in_rev = get_copyfrom(target_url)
       
  1210             if source_path == cf_source_path:
       
  1211                 report('the merge source "%s" is the copy source of "%s"' %
       
  1212                        (opts["source-url"], target_dir))
       
  1213                 revision_range = "1-" + cf_rev
       
  1214 
       
  1215     # When neither the merge source nor target is a copy of the other, and
       
  1216     # the user did not specify a revision range, then choose a default which is
       
  1217     # the current revision; saying, in effect, "everything has been merged, so
       
  1218     # mark as integrated up to the latest rev on source url).
       
  1219     revs = revision_range or "1-" + get_latest_rev(opts["source-url"])
       
  1220     revs = RevisionSet(revs)
       
  1221 
       
  1222     report('marking "%s" as already containing revisions "%s" of "%s"' %
       
  1223            (target_dir, revs, opts["source-url"]))
       
  1224 
       
  1225     revs = str(revs)
       
  1226     # If the local svnmerge-integrated property already has an entry
       
  1227     # for the source-pathid, simply error out.
       
  1228     if not opts["force"] and target_props.has_key(opts["source-pathid"]):
       
  1229         error('Repository-relative path %s has already been initialized at %s\n'
       
  1230               'Use --force to re-initialize'
       
  1231               % (opts["source-pathid"], target_dir))
       
  1232     target_props[opts["source-pathid"]] = revs
       
  1233 
       
  1234     # Set property
       
  1235     set_merge_props(target_dir, target_props)
       
  1236 
       
  1237     # Write out commit message if desired
       
  1238     if opts["commit-file"]:
       
  1239         f = open(opts["commit-file"], "w")
       
  1240         print >>f, 'Initialized merge tracking via "%s" with revisions "%s" from ' \
       
  1241             % (NAME, revs)
       
  1242         print >>f, '%s' % opts["source-url"]
       
  1243         f.close()
       
  1244         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1245 
       
  1246 def action_avail(branch_dir, branch_props):
       
  1247     """Show commits available for merges."""
       
  1248     source_revs, phantom_revs, reflected_revs, initialized_revs = \
       
  1249                analyze_source_revs(branch_dir, opts["source-url"],
       
  1250                                    find_reflected=
       
  1251                                        should_find_reflected(branch_dir))
       
  1252     report('skipping phantom revisions: %s' % phantom_revs)
       
  1253     if reflected_revs:
       
  1254         report('skipping reflected revisions: %s' % reflected_revs)
       
  1255         report('skipping initialized revisions: %s' % initialized_revs)
       
  1256 
       
  1257     blocked_revs = get_blocked_revs(branch_dir, opts["source-pathid"])
       
  1258     avail_revs = source_revs - opts["merged-revs"] - blocked_revs - \
       
  1259                  reflected_revs - initialized_revs
       
  1260 
       
  1261     # Compose the set of revisions to show
       
  1262     revs = RevisionSet("")
       
  1263     report_msg = "revisions available to be merged are:"
       
  1264     if "avail" in opts["avail-showwhat"]:
       
  1265         revs |= avail_revs
       
  1266     if "blocked" in opts["avail-showwhat"]:
       
  1267         revs |= blocked_revs
       
  1268         report_msg = "revisions blocked are:"
       
  1269 
       
  1270     # Limit to revisions specified by -r (if any)
       
  1271     if opts["revision"]:
       
  1272         revs = revs & RevisionSet(opts["revision"])
       
  1273 
       
  1274     display_revisions(revs, opts["avail-display"],
       
  1275                       report_msg,
       
  1276                       opts["source-url"])
       
  1277 
       
  1278 def action_integrated(branch_dir, branch_props):
       
  1279     """Show change sets already merged.  This set of revisions is
       
  1280     calculated from taking svnmerge-integrated property from the
       
  1281     branch, and subtracting any revision older than the branch
       
  1282     creation revision."""
       
  1283     # Extract the integration info for the branch_dir
       
  1284     branch_props = get_merge_props(branch_dir)
       
  1285     check_old_prop_version(branch_dir, branch_props)
       
  1286     revs = merge_props_to_revision_set(branch_props, opts["source-pathid"])
       
  1287 
       
  1288     # Lookup the oldest revision on the branch path.
       
  1289     oldest_src_rev = get_created_rev(opts["source-url"])
       
  1290 
       
  1291     # Subtract any revisions which pre-date the branch.
       
  1292     report("subtracting revisions which pre-date the source URL (%d)" %
       
  1293            oldest_src_rev)
       
  1294     revs = revs - RevisionSet(range(1, oldest_src_rev))
       
  1295 
       
  1296     # Limit to revisions specified by -r (if any)
       
  1297     if opts["revision"]:
       
  1298         revs = revs & RevisionSet(opts["revision"])
       
  1299 
       
  1300     display_revisions(revs, opts["integrated-display"],
       
  1301                       "revisions already integrated are:", opts["source-url"])
       
  1302 
       
  1303 def action_merge(branch_dir, branch_props):
       
  1304     """Record merge meta data, and do the actual merge (if not
       
  1305     requested otherwise via --record-only)."""
       
  1306     # Check branch directory is ready for being modified
       
  1307     check_dir_clean(branch_dir)
       
  1308 
       
  1309     source_revs, phantom_revs, reflected_revs, initialized_revs = \
       
  1310                analyze_source_revs(branch_dir, opts["source-url"],
       
  1311                                    find_reflected=
       
  1312                                        should_find_reflected(branch_dir))
       
  1313 
       
  1314     if opts["revision"]:
       
  1315         revs = RevisionSet(opts["revision"])
       
  1316     else:
       
  1317         revs = source_revs
       
  1318 
       
  1319     blocked_revs = get_blocked_revs(branch_dir, opts["source-pathid"])
       
  1320     merged_revs = opts["merged-revs"]
       
  1321 
       
  1322     # Show what we're doing
       
  1323     if opts["verbose"]:  # just to avoid useless calculations
       
  1324         if merged_revs & revs:
       
  1325             report('"%s" already contains revisions %s' % (branch_dir,
       
  1326                                                            merged_revs & revs))
       
  1327         if phantom_revs:
       
  1328             report('memorizing phantom revision(s): %s' % phantom_revs)
       
  1329         if reflected_revs:
       
  1330             report('memorizing reflected revision(s): %s' % reflected_revs)
       
  1331         if blocked_revs & revs:
       
  1332             report('skipping blocked revisions(s): %s' % (blocked_revs & revs))
       
  1333         if initialized_revs:
       
  1334             report('skipping initialized revision(s): %s' % initialized_revs)
       
  1335 
       
  1336     # Compute final merge set.
       
  1337     revs = revs - merged_revs - blocked_revs - reflected_revs - \
       
  1338            phantom_revs - initialized_revs
       
  1339     if not revs:
       
  1340         report('no revisions to merge, exiting')
       
  1341         return
       
  1342 
       
  1343     # When manually marking revisions as merged, we only update the
       
  1344     # integration meta data, and don't perform an actual merge.
       
  1345     record_only = opts["record-only"]
       
  1346 
       
  1347     if record_only:
       
  1348         report('recording merge of revision(s) %s from "%s"' %
       
  1349                (revs, opts["source-url"]))
       
  1350     else:
       
  1351         report('merging in revision(s) %s from "%s"' %
       
  1352                (revs, opts["source-url"]))
       
  1353 
       
  1354     # Do the merge(s). Note: the starting revision number to 'svn merge'
       
  1355     # is NOT inclusive so we have to subtract one from start.
       
  1356     # We try to keep the number of merge operations as low as possible,
       
  1357     # because it is faster and reduces the number of conflicts.
       
  1358     old_block_props = get_block_props(branch_dir)
       
  1359     merge_metadata = logs[opts["source-url"]].merge_metadata()
       
  1360     block_metadata = logs[opts["source-url"]].block_metadata()
       
  1361     for start,end in minimal_merge_intervals(revs, phantom_revs):
       
  1362         if not record_only:
       
  1363             # Preset merge/blocked properties to the source value at
       
  1364             # the start rev to avoid spurious property conflicts
       
  1365             set_merge_props(branch_dir, merge_metadata.get(start - 1))
       
  1366             set_block_props(branch_dir, block_metadata.get(start - 1))
       
  1367             # Do the merge
       
  1368             svn_command("merge --force -r %d:%d %s %s" % \
       
  1369                         (start - 1, end, opts["source-url"], branch_dir))
       
  1370             # TODO: to support graph merging, add logic to merge the property
       
  1371             # meta-data manually
       
  1372 
       
  1373     # Update the set of merged revisions.
       
  1374     merged_revs = merged_revs | revs | reflected_revs | phantom_revs | initialized_revs
       
  1375     branch_props[opts["source-pathid"]] = str(merged_revs)
       
  1376     set_merge_props(branch_dir, branch_props)
       
  1377     # Reset the blocked revs
       
  1378     set_block_props(branch_dir, old_block_props)
       
  1379 
       
  1380     # Write out commit message if desired
       
  1381     if opts["commit-file"]:
       
  1382         f = open(opts["commit-file"], "w")
       
  1383         if record_only:
       
  1384             print >>f, 'Recorded merge of revisions %s via %s from ' % \
       
  1385                   (revs, NAME)
       
  1386         else:
       
  1387             print >>f, 'Merged revisions %s via %s from ' % \
       
  1388                   (revs, NAME)
       
  1389         print >>f, '%s' % opts["source-url"]
       
  1390         if opts["commit-verbose"]:
       
  1391             print >>f
       
  1392             print >>f, construct_merged_log_message(opts["source-url"], revs),
       
  1393 
       
  1394         f.close()
       
  1395         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1396 
       
  1397 def action_block(branch_dir, branch_props):
       
  1398     """Block revisions."""
       
  1399     # Check branch directory is ready for being modified
       
  1400     check_dir_clean(branch_dir)
       
  1401 
       
  1402     source_revs, phantom_revs, reflected_revs, initialized_revs = \
       
  1403                analyze_source_revs(branch_dir, opts["source-url"])
       
  1404     revs_to_block = source_revs - opts["merged-revs"]
       
  1405 
       
  1406     # Limit to revisions specified by -r (if any)
       
  1407     if opts["revision"]:
       
  1408         revs_to_block = RevisionSet(opts["revision"]) & revs_to_block
       
  1409 
       
  1410     if not revs_to_block:
       
  1411         error('no available revisions to block')
       
  1412 
       
  1413     # Change blocked information
       
  1414     blocked_revs = get_blocked_revs(branch_dir, opts["source-pathid"])
       
  1415     blocked_revs = blocked_revs | revs_to_block
       
  1416     set_blocked_revs(branch_dir, opts["source-pathid"], blocked_revs)
       
  1417 
       
  1418     # Write out commit message if desired
       
  1419     if opts["commit-file"]:
       
  1420         f = open(opts["commit-file"], "w")
       
  1421         print >>f, 'Blocked revisions %s via %s' % (revs_to_block, NAME)
       
  1422         if opts["commit-verbose"]:
       
  1423             print >>f
       
  1424             print >>f, construct_merged_log_message(opts["source-url"],
       
  1425                                                     revs_to_block),
       
  1426 
       
  1427         f.close()
       
  1428         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1429 
       
  1430 def action_unblock(branch_dir, branch_props):
       
  1431     """Unblock revisions."""
       
  1432     # Check branch directory is ready for being modified
       
  1433     check_dir_clean(branch_dir)
       
  1434 
       
  1435     blocked_revs = get_blocked_revs(branch_dir, opts["source-pathid"])
       
  1436     revs_to_unblock = blocked_revs
       
  1437 
       
  1438     # Limit to revisions specified by -r (if any)
       
  1439     if opts["revision"]:
       
  1440         revs_to_unblock = revs_to_unblock & RevisionSet(opts["revision"])
       
  1441 
       
  1442     if not revs_to_unblock:
       
  1443         error('no available revisions to unblock')
       
  1444 
       
  1445     # Change blocked information
       
  1446     blocked_revs = blocked_revs - revs_to_unblock
       
  1447     set_blocked_revs(branch_dir, opts["source-pathid"], blocked_revs)
       
  1448 
       
  1449     # Write out commit message if desired
       
  1450     if opts["commit-file"]:
       
  1451         f = open(opts["commit-file"], "w")
       
  1452         print >>f, 'Unblocked revisions %s via %s' % (revs_to_unblock, NAME)
       
  1453         if opts["commit-verbose"]:
       
  1454             print >>f
       
  1455             print >>f, construct_merged_log_message(opts["source-url"],
       
  1456                                                     revs_to_unblock),
       
  1457         f.close()
       
  1458         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1459 
       
  1460 def action_rollback(branch_dir, branch_props):
       
  1461     """Rollback previously integrated revisions."""
       
  1462 
       
  1463     # Make sure the revision arguments are present
       
  1464     if not opts["revision"]:
       
  1465         error("The '-r' option is mandatory for rollback")
       
  1466 
       
  1467     # Check branch directory is ready for being modified
       
  1468     check_dir_clean(branch_dir)
       
  1469 
       
  1470     # Extract the integration info for the branch_dir
       
  1471     branch_props = get_merge_props(branch_dir)
       
  1472     check_old_prop_version(branch_dir, branch_props)
       
  1473     # Get the list of all revisions already merged into this source-pathid.
       
  1474     merged_revs = merge_props_to_revision_set(branch_props,
       
  1475                                               opts["source-pathid"])
       
  1476 
       
  1477     # At which revision was the src created?
       
  1478     oldest_src_rev = get_created_rev(opts["source-url"])
       
  1479     src_pre_exist_range = RevisionSet("1-%d" % oldest_src_rev)
       
  1480 
       
  1481     # Limit to revisions specified by -r (if any)
       
  1482     revs = merged_revs & RevisionSet(opts["revision"])
       
  1483 
       
  1484     # make sure there's some revision to rollback
       
  1485     if not revs:
       
  1486         report("Nothing to rollback in revision range r%s" % opts["revision"])
       
  1487         return
       
  1488 
       
  1489     # If even one specified revision lies outside the lifetime of the
       
  1490     # merge source, error out.
       
  1491     if revs & src_pre_exist_range:
       
  1492         err_str  = "Specified revision range falls out of the rollback range.\n"
       
  1493         err_str += "%s was created at r%d" % (opts["source-pathid"],
       
  1494                                               oldest_src_rev)
       
  1495         error(err_str)
       
  1496 
       
  1497     record_only = opts["record-only"]
       
  1498 
       
  1499     if record_only:
       
  1500         report('recording rollback of revision(s) %s from "%s"' %
       
  1501                (revs, opts["source-url"]))
       
  1502     else:
       
  1503         report('rollback of revision(s) %s from "%s"' %
       
  1504                (revs, opts["source-url"]))
       
  1505 
       
  1506     # Do the reverse merge(s). Note: the starting revision number
       
  1507     # to 'svn merge' is NOT inclusive so we have to subtract one from start.
       
  1508     # We try to keep the number of merge operations as low as possible,
       
  1509     # because it is faster and reduces the number of conflicts.
       
  1510     rollback_intervals = minimal_merge_intervals(revs, [])
       
  1511     # rollback in the reverse order of merge
       
  1512     rollback_intervals.reverse()
       
  1513     for start, end in rollback_intervals:
       
  1514         if not record_only:
       
  1515             # Do the merge
       
  1516             svn_command("merge --force -r %d:%d %s %s" % \
       
  1517                         (end, start - 1, opts["source-url"], branch_dir))
       
  1518 
       
  1519     # Write out commit message if desired
       
  1520     # calculate the phantom revs first
       
  1521     if opts["commit-file"]:
       
  1522         f = open(opts["commit-file"], "w")
       
  1523         if record_only:
       
  1524             print >>f, 'Recorded rollback of revisions %s via %s from ' % \
       
  1525                   (revs , NAME)
       
  1526         else:
       
  1527             print >>f, 'Rolled back revisions %s via %s from ' % \
       
  1528                   (revs , NAME)
       
  1529         print >>f, '%s' % opts["source-url"]
       
  1530 
       
  1531         f.close()
       
  1532         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1533 
       
  1534     # Update the set of merged revisions.
       
  1535     merged_revs = merged_revs - revs
       
  1536     branch_props[opts["source-pathid"]] = str(merged_revs)
       
  1537     set_merge_props(branch_dir, branch_props)
       
  1538 
       
  1539 def action_uninit(branch_dir, branch_props):
       
  1540     """Uninit SOURCE URL."""
       
  1541     # Check branch directory is ready for being modified
       
  1542     check_dir_clean(branch_dir)
       
  1543 
       
  1544     # If the source-pathid does not have an entry in the svnmerge-integrated
       
  1545     # property, simply error out.
       
  1546     if not branch_props.has_key(opts["source-pathid"]):
       
  1547         error('Repository-relative path "%s" does not contain merge '
       
  1548               'tracking information for "%s"' \
       
  1549                 % (opts["source-pathid"], branch_dir))
       
  1550 
       
  1551     del branch_props[opts["source-pathid"]]
       
  1552 
       
  1553     # Set merge property with the selected source deleted
       
  1554     set_merge_props(branch_dir, branch_props)
       
  1555 
       
  1556     # Set blocked revisions for the selected source to None
       
  1557     set_blocked_revs(branch_dir, opts["source-pathid"], None)
       
  1558 
       
  1559     # Write out commit message if desired
       
  1560     if opts["commit-file"]:
       
  1561         f = open(opts["commit-file"], "w")
       
  1562         print >>f, 'Removed merge tracking for "%s" for ' % NAME
       
  1563         print >>f, '%s' % opts["source-url"]
       
  1564         f.close()
       
  1565         report('wrote commit message to "%s"' % opts["commit-file"])
       
  1566 
       
  1567 ###############################################################################
       
  1568 # Command line parsing -- options and commands management
       
  1569 ###############################################################################
       
  1570 
       
  1571 class OptBase:
       
  1572     def __init__(self, *args, **kwargs):
       
  1573         self.help = kwargs["help"]
       
  1574         del kwargs["help"]
       
  1575         self.lflags = []
       
  1576         self.sflags = []
       
  1577         for a in args:
       
  1578             if a.startswith("--"):   self.lflags.append(a)
       
  1579             elif a.startswith("-"):  self.sflags.append(a)
       
  1580             else:
       
  1581                 raise TypeError, "invalid flag name: %s" % a
       
  1582         if kwargs.has_key("dest"):
       
  1583             self.dest = kwargs["dest"]
       
  1584             del kwargs["dest"]
       
  1585         else:
       
  1586             if not self.lflags:
       
  1587                 raise TypeError, "cannot deduce dest name without long options"
       
  1588             self.dest = self.lflags[0][2:]
       
  1589         if kwargs:
       
  1590             raise TypeError, "invalid keyword arguments: %r" % kwargs.keys()
       
  1591     def repr_flags(self):
       
  1592         f = self.sflags + self.lflags
       
  1593         r = f[0]
       
  1594         for fl in f[1:]:
       
  1595             r += " [%s]" % fl
       
  1596         return r
       
  1597 
       
  1598 class Option(OptBase):
       
  1599     def __init__(self, *args, **kwargs):
       
  1600         self.default = kwargs.setdefault("default", 0)
       
  1601         del kwargs["default"]
       
  1602         self.value = kwargs.setdefault("value", None)
       
  1603         del kwargs["value"]
       
  1604         OptBase.__init__(self, *args, **kwargs)
       
  1605     def apply(self, state, value):
       
  1606         assert value == ""
       
  1607         if self.value is not None:
       
  1608             state[self.dest] = self.value
       
  1609         else:
       
  1610             state[self.dest] += 1
       
  1611 
       
  1612 class OptionArg(OptBase):
       
  1613     def __init__(self, *args, **kwargs):
       
  1614         self.default = kwargs["default"]
       
  1615         del kwargs["default"]
       
  1616         self.metavar = kwargs.setdefault("metavar", None)
       
  1617         del kwargs["metavar"]
       
  1618         OptBase.__init__(self, *args, **kwargs)
       
  1619 
       
  1620         if self.metavar is None:
       
  1621             if self.dest is not None:
       
  1622                 self.metavar = self.dest.upper()
       
  1623             else:
       
  1624                 self.metavar = "arg"
       
  1625         if self.default:
       
  1626             self.help += " (default: %s)" % self.default
       
  1627     def apply(self, state, value):
       
  1628         assert value is not None
       
  1629         state[self.dest] = value
       
  1630     def repr_flags(self):
       
  1631         r = OptBase.repr_flags(self)
       
  1632         return r + " " + self.metavar
       
  1633 
       
  1634 class CommandOpts:
       
  1635     class Cmd:
       
  1636         def __init__(self, *args):
       
  1637             self.name, self.func, self.usage, self.help, self.opts = args
       
  1638         def short_help(self):
       
  1639             return self.help.split(".")[0]
       
  1640         def __str__(self):
       
  1641             return self.name
       
  1642         def __call__(self, *args, **kwargs):
       
  1643             return self.func(*args, **kwargs)
       
  1644 
       
  1645     def __init__(self, global_opts, common_opts, command_table, version=None):
       
  1646         self.progname = NAME
       
  1647         self.version = version.replace("%prog", self.progname)
       
  1648         self.cwidth = console_width() - 2
       
  1649         self.ctable = command_table.copy()
       
  1650         self.gopts = global_opts[:]
       
  1651         self.copts = common_opts[:]
       
  1652         self._add_builtins()
       
  1653         for k in self.ctable.keys():
       
  1654             cmd = self.Cmd(k, *self.ctable[k])
       
  1655             opts = []
       
  1656             for o in cmd.opts:
       
  1657                 if isinstance(o, types.StringType) or \
       
  1658                    isinstance(o, types.UnicodeType):
       
  1659                     o = self._find_common(o)
       
  1660                 opts.append(o)
       
  1661             cmd.opts = opts
       
  1662             self.ctable[k] = cmd
       
  1663 
       
  1664     def _add_builtins(self):
       
  1665         self.gopts.append(
       
  1666             Option("-h", "--help", help="show help for this command and exit"))
       
  1667         if self.version is not None:
       
  1668             self.gopts.append(
       
  1669                 Option("-V", "--version", help="show version info and exit"))
       
  1670         self.ctable["help"] = (self._cmd_help,
       
  1671             "help [COMMAND]",
       
  1672             "Display help for a specific command. If COMMAND is omitted, "
       
  1673             "display brief command description.",
       
  1674             [])
       
  1675 
       
  1676     def _cmd_help(self, cmd=None, *args):
       
  1677         if args:
       
  1678             self.error("wrong number of arguments", "help")
       
  1679         if cmd is not None:
       
  1680             cmd = self._command(cmd)
       
  1681             self.print_command_help(cmd)
       
  1682         else:
       
  1683             self.print_command_list()
       
  1684 
       
  1685     def _paragraph(self, text, width=78):
       
  1686         chunks = re.split("\s+", text.strip())
       
  1687         chunks.reverse()
       
  1688         lines = []
       
  1689         while chunks:
       
  1690             L = chunks.pop()
       
  1691             while chunks and len(L) + len(chunks[-1]) + 1 <= width:
       
  1692                 L += " " + chunks.pop()
       
  1693             lines.append(L)
       
  1694         return lines
       
  1695 
       
  1696     def _paragraphs(self, text, *args, **kwargs):
       
  1697         pars = text.split("\n\n")
       
  1698         lines = self._paragraph(pars[0], *args, **kwargs)
       
  1699         for p in pars[1:]:
       
  1700             lines.append("")
       
  1701             lines.extend(self._paragraph(p, *args, **kwargs))
       
  1702         return lines
       
  1703 
       
  1704     def _print_wrapped(self, text, indent=0):
       
  1705         text = self._paragraphs(text, self.cwidth - indent)
       
  1706         print text.pop(0)
       
  1707         for t in text:
       
  1708             print " " * indent + t
       
  1709 
       
  1710     def _find_common(self, fl):
       
  1711         for o in self.copts:
       
  1712             if fl in o.lflags+o.sflags:
       
  1713                 return o
       
  1714         assert False, fl
       
  1715 
       
  1716     def _compute_flags(self, opts, check_conflicts=True):
       
  1717         back = {}
       
  1718         sfl = ""
       
  1719         lfl = []
       
  1720         for o in opts:
       
  1721             sapp = lapp = ""
       
  1722             if isinstance(o, OptionArg):
       
  1723                 sapp, lapp = ":", "="
       
  1724             for s in o.sflags:
       
  1725                 if check_conflicts and back.has_key(s):
       
  1726                     raise RuntimeError, "option conflict: %s" % s
       
  1727                 back[s] = o
       
  1728                 sfl += s[1:] + sapp
       
  1729             for l in o.lflags:
       
  1730                 if check_conflicts and back.has_key(l):
       
  1731                     raise RuntimeError, "option conflict: %s" % l
       
  1732                 back[l] = o
       
  1733                 lfl.append(l[2:] + lapp)
       
  1734         return sfl, lfl, back
       
  1735 
       
  1736     def _extract_command(self, args):
       
  1737         """
       
  1738         Try to extract the command name from the argument list. This is
       
  1739         non-trivial because we want to allow command-specific options even
       
  1740         before the command itself.
       
  1741         """
       
  1742         opts = self.gopts[:]
       
  1743         for cmd in self.ctable.values():
       
  1744             opts.extend(cmd.opts)
       
  1745         sfl, lfl, _ = self._compute_flags(opts, check_conflicts=False)
       
  1746 
       
  1747         lopts,largs = getopt.getopt(args, sfl, lfl)
       
  1748         if not largs:
       
  1749             return None
       
  1750         return self._command(largs[0])
       
  1751 
       
  1752     def _fancy_getopt(self, args, opts, state=None):
       
  1753         if state is None:
       
  1754             state= {}
       
  1755         for o in opts:
       
  1756             if not state.has_key(o.dest):
       
  1757                 state[o.dest] = o.default
       
  1758 
       
  1759         sfl, lfl, back = self._compute_flags(opts)
       
  1760         try:
       
  1761             lopts,args = getopt.gnu_getopt(args, sfl, lfl)
       
  1762         except AttributeError:
       
  1763             # Before Python 2.3, there was no gnu_getopt support.
       
  1764             # So we can't parse intermixed positional arguments
       
  1765             # and options.
       
  1766             lopts,args = getopt.getopt(args, sfl, lfl)
       
  1767 
       
  1768         for o,v in lopts:
       
  1769             back[o].apply(state, v)
       
  1770         return state, args
       
  1771 
       
  1772     def _command(self, cmd):
       
  1773         if not self.ctable.has_key(cmd):
       
  1774             self.error("unknown command: '%s'" % cmd)
       
  1775         return self.ctable[cmd]
       
  1776 
       
  1777     def parse(self, args):
       
  1778         if not args:
       
  1779             self.print_small_help()
       
  1780             sys.exit(0)
       
  1781 
       
  1782         cmd = None
       
  1783         try:
       
  1784             cmd = self._extract_command(args)
       
  1785             opts = self.gopts[:]
       
  1786             if cmd:
       
  1787                 opts.extend(cmd.opts)
       
  1788                 args.remove(cmd.name)
       
  1789             state, args = self._fancy_getopt(args, opts)
       
  1790         except getopt.GetoptError, e:
       
  1791             self.error(e, cmd)
       
  1792 
       
  1793         # Handle builtins
       
  1794         if self.version is not None and state["version"]:
       
  1795             self.print_version()
       
  1796             sys.exit(0)
       
  1797         if state["help"]: # special case for --help
       
  1798             if cmd:
       
  1799                 self.print_command_help(cmd)
       
  1800                 sys.exit(0)
       
  1801             cmd = self.ctable["help"]
       
  1802         else:
       
  1803             if cmd is None:
       
  1804                 self.error("command argument required")
       
  1805         if str(cmd) == "help":
       
  1806             cmd(*args)
       
  1807             sys.exit(0)
       
  1808         return cmd, args, state
       
  1809 
       
  1810     def error(self, s, cmd=None):
       
  1811         print >>sys.stderr, "%s: %s" % (self.progname, s)
       
  1812         if cmd is not None:
       
  1813             self.print_command_help(cmd)
       
  1814         else:
       
  1815             self.print_small_help()
       
  1816         sys.exit(1)
       
  1817     def print_small_help(self):
       
  1818         print "Type '%s help' for usage" % self.progname
       
  1819     def print_usage_line(self):
       
  1820         print "usage: %s <subcommand> [options...] [args...]\n" % self.progname
       
  1821     def print_command_list(self):
       
  1822         print "Available commands (use '%s help COMMAND' for more details):\n" \
       
  1823               % self.progname
       
  1824         cmds = self.ctable.keys()
       
  1825         cmds.sort()
       
  1826         indent = max(map(len, cmds))
       
  1827         for c in cmds:
       
  1828             h = self.ctable[c].short_help()
       
  1829             print "  %-*s   " % (indent, c),
       
  1830             self._print_wrapped(h, indent+6)
       
  1831     def print_command_help(self, cmd):
       
  1832         cmd = self.ctable[str(cmd)]
       
  1833         print 'usage: %s %s\n' % (self.progname, cmd.usage)
       
  1834         self._print_wrapped(cmd.help)
       
  1835         def print_opts(opts, self=self):
       
  1836             if not opts: return
       
  1837             flags = [o.repr_flags() for o in opts]
       
  1838             indent = max(map(len, flags))
       
  1839             for f,o in zip(flags, opts):
       
  1840                 print "  %-*s :" % (indent, f),
       
  1841                 self._print_wrapped(o.help, indent+5)
       
  1842         print '\nCommand options:'
       
  1843         print_opts(cmd.opts)
       
  1844         print '\nGlobal options:'
       
  1845         print_opts(self.gopts)
       
  1846 
       
  1847     def print_version(self):
       
  1848         print self.version
       
  1849 
       
  1850 ###############################################################################
       
  1851 # Options and Commands description
       
  1852 ###############################################################################
       
  1853 
       
  1854 global_opts = [
       
  1855     Option("-F", "--force",
       
  1856            help="force operation even if the working copy is not clean, or "
       
  1857                 "there are pending updates"),
       
  1858     Option("-n", "--dry-run",
       
  1859            help="don't actually change anything, just pretend; "
       
  1860                 "implies --show-changes"),
       
  1861     Option("-s", "--show-changes",
       
  1862            help="show subversion commands that make changes"),
       
  1863     Option("-v", "--verbose",
       
  1864            help="verbose mode: output more information about progress"),
       
  1865     OptionArg("-u", "--username",
       
  1866               default=None,
       
  1867               help="invoke subversion commands with the supplied username"),
       
  1868     OptionArg("-p", "--password",
       
  1869               default=None,
       
  1870               help="invoke subversion commands with the supplied password"),
       
  1871 ]
       
  1872 
       
  1873 common_opts = [
       
  1874     Option("-b", "--bidirectional",
       
  1875            value=True,
       
  1876            default=False,
       
  1877            help="remove reflected and initialized revisions from merge candidates.  "
       
  1878                 "Not required but may be specified to speed things up slightly"),
       
  1879     OptionArg("-f", "--commit-file", metavar="FILE",
       
  1880               default="svnmerge-commit-message.txt",
       
  1881               help="set the name of the file where the suggested log message "
       
  1882                    "is written to"),
       
  1883     Option("-M", "--record-only",
       
  1884            value=True,
       
  1885            default=False,
       
  1886            help="do not perform an actual merge of the changes, yet record "
       
  1887                 "that a merge happened"),
       
  1888     OptionArg("-r", "--revision",
       
  1889               metavar="REVLIST",
       
  1890               default="",
       
  1891               help="specify a revision list, consisting of revision numbers "
       
  1892                    'and ranges separated by commas, e.g., "534,537-539,540"'),
       
  1893     OptionArg("-S", "--source", "--head",
       
  1894               default=None,
       
  1895               help="specify a merge source for this branch.  It can be either "
       
  1896                    "a path, a full URL, or an unambiguous substring of one "
       
  1897                    "of the paths for which merge tracking was already "
       
  1898                    "initialized.  Needed only to disambiguate in case of "
       
  1899                    "multiple merge sources"),
       
  1900 ]
       
  1901 
       
  1902 command_table = {
       
  1903     "init": (action_init,
       
  1904     "init [OPTION...] [SOURCE]",
       
  1905     """Initialize merge tracking from SOURCE on the current working
       
  1906     directory.
       
  1907 
       
  1908     If SOURCE is specified, all the revisions in SOURCE are marked as already
       
  1909     merged; if this is not correct, you can use --revision to specify the
       
  1910     exact list of already-merged revisions.
       
  1911 
       
  1912     If SOURCE is omitted, then it is computed from the "svn cp" history of the
       
  1913     current working directory (searching back for the branch point); in this
       
  1914     case, %s assumes that no revision has been integrated yet since
       
  1915     the branch point (unless you teach it with --revision).""" % NAME,
       
  1916     [
       
  1917         "-f", "-r", # import common opts
       
  1918     ]),
       
  1919 
       
  1920     "avail": (action_avail,
       
  1921     "avail [OPTION...] [PATH]",
       
  1922     """Show unmerged revisions available for PATH as a revision list.
       
  1923     If --revision is given, the revisions shown will be limited to those
       
  1924     also specified in the option.
       
  1925 
       
  1926     When svnmerge is used to bidirectionally merge changes between a
       
  1927     branch and its source, it is necessary to not merge the same changes
       
  1928     forth and back: e.g., if you committed a merge of a certain
       
  1929     revision of the branch into the source, you do not want that commit
       
  1930     to appear as available to merged into the branch (as the code
       
  1931     originated in the branch itself!).  svnmerge will automatically
       
  1932     exclude these so-called "reflected" revisions.""",
       
  1933     [
       
  1934         Option("-A", "--all",
       
  1935                dest="avail-showwhat",
       
  1936                value=["blocked", "avail"],
       
  1937                default=["avail"],
       
  1938                help="show both available and blocked revisions (aka ignore "
       
  1939                     "blocked revisions)"),
       
  1940         "-b",
       
  1941         Option("-B", "--blocked",
       
  1942                dest="avail-showwhat",
       
  1943                value=["blocked"],
       
  1944                help="show the blocked revision list (see '%s block')" % NAME),
       
  1945         Option("-d", "--diff",
       
  1946                dest="avail-display",
       
  1947                value="diffs",
       
  1948                default="revisions",
       
  1949                help="show corresponding diff instead of revision list"),
       
  1950         Option("--summarize",
       
  1951                dest="avail-display",
       
  1952                value="summarize",
       
  1953                help="show summarized diff instead of revision list"),
       
  1954         Option("-l", "--log",
       
  1955                dest="avail-display",
       
  1956                value="logs",
       
  1957                help="show corresponding log history instead of revision list"),
       
  1958         "-r",
       
  1959         "-S",
       
  1960     ]),
       
  1961 
       
  1962     "integrated": (action_integrated,
       
  1963     "integrated [OPTION...] [PATH]",
       
  1964     """Show merged revisions available for PATH as a revision list.
       
  1965     If --revision is given, the revisions shown will be limited to
       
  1966     those also specified in the option.""",
       
  1967     [
       
  1968         Option("-d", "--diff",
       
  1969                dest="integrated-display",
       
  1970                value="diffs",
       
  1971                default="revisions",
       
  1972                help="show corresponding diff instead of revision list"),
       
  1973         Option("-l", "--log",
       
  1974                dest="integrated-display",
       
  1975                value="logs",
       
  1976                help="show corresponding log history instead of revision list"),
       
  1977         "-r",
       
  1978         "-S",
       
  1979     ]),
       
  1980 
       
  1981     "rollback": (action_rollback,
       
  1982     "rollback [OPTION...] [PATH]",
       
  1983     """Rollback previously merged in revisions from PATH.  The
       
  1984     --revision option is mandatory, and specifies which revisions
       
  1985     will be rolled back.  Only the previously integrated merges
       
  1986     will be rolled back.
       
  1987 
       
  1988     When manually rolling back changes, --record-only can be used to
       
  1989     instruct %s that a manual rollback of a certain revision
       
  1990     already happened, so that it can record it and offer that
       
  1991     revision for merge henceforth.""" % (NAME),
       
  1992     [
       
  1993         "-f", "-r", "-S", "-M", # import common opts
       
  1994     ]),
       
  1995 
       
  1996     "merge": (action_merge,
       
  1997     "merge [OPTION...] [PATH]",
       
  1998     """Merge in revisions into PATH from its source. If --revision is omitted,
       
  1999     all the available revisions will be merged. In any case, already merged-in
       
  2000     revisions will NOT be merged again.
       
  2001 
       
  2002     When svnmerge is used to bidirectionally merge changes between a
       
  2003     branch and its source, it is necessary to not merge the same changes
       
  2004     forth and back: e.g., if you committed a merge of a certain
       
  2005     revision of the branch into the source, you do not want that commit
       
  2006     to appear as available to merged into the branch (as the code
       
  2007     originated in the branch itself!).  svnmerge will automatically
       
  2008     exclude these so-called "reflected" revisions.
       
  2009 
       
  2010     When manually merging changes across branches, --record-only can
       
  2011     be used to instruct %s that a manual merge of a certain revision
       
  2012     already happened, so that it can record it and not offer that
       
  2013     revision for merge anymore.  Conversely, when there are revisions
       
  2014     which should not be merged, use '%s block'.""" % (NAME, NAME),
       
  2015     [
       
  2016         "-b", "-f", "-r", "-S", "-M", # import common opts
       
  2017     ]),
       
  2018 
       
  2019     "block": (action_block,
       
  2020     "block [OPTION...] [PATH]",
       
  2021     """Block revisions within PATH so that they disappear from the available
       
  2022     list. This is useful to hide revisions which will not be integrated.
       
  2023     If --revision is omitted, it defaults to all the available revisions.
       
  2024 
       
  2025     Do not use this option to hide revisions that were manually merged
       
  2026     into the branch.  Instead, use '%s merge --record-only', which
       
  2027     records that a merge happened (as opposed to a merge which should
       
  2028     not happen).""" % NAME,
       
  2029     [
       
  2030         "-f", "-r", "-S", # import common opts
       
  2031     ]),
       
  2032 
       
  2033     "unblock": (action_unblock,
       
  2034     "unblock [OPTION...] [PATH]",
       
  2035     """Revert the effect of '%s block'. If --revision is omitted, all the
       
  2036     blocked revisions are unblocked""" % NAME,
       
  2037     [
       
  2038         "-f", "-r", "-S", # import common opts
       
  2039     ]),
       
  2040 
       
  2041     "uninit": (action_uninit,
       
  2042     "uninit [OPTION...] [PATH]",
       
  2043     """Remove merge tracking information from PATH. It cleans any kind of merge
       
  2044     tracking information (including the list of blocked revisions). If there
       
  2045     are multiple sources, use --source to indicate which source you want to
       
  2046     forget about.""",
       
  2047     [
       
  2048         "-f", "-S", # import common opts
       
  2049     ]),
       
  2050 }
       
  2051 
       
  2052 
       
  2053 def main(args):
       
  2054     global opts
       
  2055 
       
  2056     # Initialize default options
       
  2057     opts = default_opts.copy()
       
  2058     logs.clear()
       
  2059 
       
  2060     optsparser = CommandOpts(global_opts, common_opts, command_table,
       
  2061                              version="%%prog r%s\n  modified: %s\n\n"
       
  2062                                      "Copyright (C) 2004,2005 Awarix Inc.\n"
       
  2063                                      "Copyright (C) 2005, Giovanni Bajo"
       
  2064                                      % (__revision__, __date__))
       
  2065 
       
  2066     cmd, args, state = optsparser.parse(args)
       
  2067     opts.update(state)
       
  2068 
       
  2069     source = opts.get("source", None)
       
  2070     branch_dir = "."
       
  2071 
       
  2072     if str(cmd) == "init":
       
  2073         if len(args) == 1:
       
  2074             source = args[0]
       
  2075         elif len(args) > 1:
       
  2076             optsparser.error("wrong number of parameters", cmd)
       
  2077     elif str(cmd) in command_table.keys():
       
  2078         if len(args) == 1:
       
  2079             branch_dir = args[0]
       
  2080         elif len(args) > 1:
       
  2081             optsparser.error("wrong number of parameters", cmd)
       
  2082     else:
       
  2083         assert False, "command not handled: %s" % cmd
       
  2084 
       
  2085     # Validate branch_dir
       
  2086     if not is_wc(branch_dir):
       
  2087         error('"%s" is not a subversion working directory' % branch_dir)
       
  2088 
       
  2089     # Extract the integration info for the branch_dir
       
  2090     branch_props = get_merge_props(branch_dir)
       
  2091     check_old_prop_version(branch_dir, branch_props)
       
  2092 
       
  2093     # Calculate source_url and source_path
       
  2094     report("calculate source path for the branch")
       
  2095     if not source:
       
  2096         if str(cmd) == "init":
       
  2097             cf_source, cf_rev, copy_committed_in_rev = get_copyfrom(branch_dir)
       
  2098             if not cf_source:
       
  2099                 error('no copyfrom info available. '
       
  2100                       'Explicit source argument (-S/--source) required.')
       
  2101             opts["source-pathid"] = cf_source
       
  2102             if not opts["revision"]:
       
  2103                 opts["revision"] = "1-" + cf_rev
       
  2104         else:
       
  2105             opts["source-pathid"] = get_default_source(branch_dir, branch_props)
       
  2106 
       
  2107         # (assumes pathid is a repository-relative-path)
       
  2108         assert opts["source-pathid"][0] == '/'
       
  2109         opts["source-url"] = get_repo_root(branch_dir) + opts["source-pathid"]
       
  2110     else:
       
  2111         # The source was given as a command line argument and is stored in
       
  2112         # SOURCE.  Ensure that the specified source does not end in a /,
       
  2113         # otherwise it's easy to have the same source path listed more
       
  2114         # than once in the integrated version properties, with and without
       
  2115         # trailing /'s.
       
  2116         source = rstrip(source, "/")
       
  2117         if not is_wc(source) and not is_url(source):
       
  2118             # Check if it is a substring of a pathid recorded
       
  2119             # within the branch properties.
       
  2120             found = []
       
  2121             for pathid in branch_props.keys():
       
  2122                 if pathid.find(source) > 0:
       
  2123                     found.append(pathid)
       
  2124             if len(found) == 1:
       
  2125                 # (assumes pathid is a repository-relative-path)
       
  2126                 source = get_repo_root(branch_dir) + found[0]
       
  2127             else:
       
  2128                 error('"%s" is neither a valid URL, nor an unambiguous '
       
  2129                       'substring of a repository path, nor a working directory'
       
  2130                       % source)
       
  2131 
       
  2132         source_pathid = target_to_pathid(source)
       
  2133         if str(cmd) == "init" and \
       
  2134                source_pathid == target_to_pathid("."):
       
  2135             error("cannot init integration source path '%s'\n"
       
  2136                   "Its repository-relative path must differ from the "
       
  2137                   "repository-relative path of the current directory."
       
  2138                   % source_pathid)
       
  2139         opts["source-pathid"] = source_pathid
       
  2140         opts["source-url"] = target_to_url(source)
       
  2141 
       
  2142     # Sanity check source_url
       
  2143     assert is_url(opts["source-url"])
       
  2144     # SVN does not support non-normalized URL (and we should not
       
  2145     # have created them)
       
  2146     assert opts["source-url"].find("/..") < 0
       
  2147 
       
  2148     report('source is "%s"' % opts["source-url"])
       
  2149 
       
  2150     # Get previously merged revisions (except when command is init)
       
  2151     if str(cmd) != "init":
       
  2152         opts["merged-revs"] = merge_props_to_revision_set(branch_props,
       
  2153                                                           opts["source-pathid"])
       
  2154 
       
  2155     # Perform the action
       
  2156     cmd(branch_dir, branch_props)
       
  2157 
       
  2158 
       
  2159 if __name__ == "__main__":
       
  2160     try:
       
  2161         main(sys.argv[1:])
       
  2162     except LaunchError, (ret, cmd, out):
       
  2163         err_msg = "command execution failed (exit code: %d)\n" % ret
       
  2164         err_msg += cmd + "\n"
       
  2165         err_msg += "".join(out)
       
  2166         error(err_msg)
       
  2167     except KeyboardInterrupt:
       
  2168         # Avoid traceback on CTRL+C
       
  2169         print "aborted by user"
       
  2170         sys.exit(1)