source: valtobtest/subversion-1.6.2/contrib/client-side/svnmerge/svnmerge.py @ 3

Last change on this file since 3 was 3, checked in by valtob, 15 years ago

subversion source 1.6.2 as test

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