2 # -*- coding: utf-8 -*-
3 # Copyright (c) 2005, Giovanni Bajo
4 # Copyright (c) 2004-2005, Awarix, Inc.
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.
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.
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
21 # Author: Archie Cobbs <archie at awarix dot com>
22 # Rewritten in Python by: Giovanni Bajo <rasky at develer dot com>
25 # John Belmonte <john at neggie dot net> - metadata and usability
27 # Blair Zajac <blair at orcaware dot com> - random improvements
28 # Raman Gupta <rocketraman at fastmail dot fm> - bidirectional and transitive
30 # Dustin J. Mitchell <dustin at zmanda dot com> - support for multiple
31 # location identifier formats
36 # $LastChangedRevision$
39 # svnmerge.py has been tested with all SVN major versions since 1.1 (both
40 # client and server). It is unknown if it works with previous versions.
42 # Differences from svnmerge.sh:
43 # - More portable: tested as working in FreeBSD and OS/2.
44 # - Add double-verbose mode, which shows every svn command executed (-v -v).
45 # - "svnmerge avail" now only shows commits in source, not also commits in
46 # other parts of the repository.
47 # - Add "svnmerge block" to flag some revisions as blocked, so that
48 # they will not show up anymore in the available list. Added also
49 # the complementary "svnmerge unblock".
50 # - "svnmerge avail" has grown two new options:
51 # -B to display a list of the blocked revisions
52 # -A to display both the blocked and the available revisions.
53 # - Improved generated commit message to make it machine parsable even when
54 # merging commits which are themselves merges.
55 # - Add --force option to skip working copy check
56 # - Add --record-only option to "svnmerge merge" to avoid performing
57 # an actual merge, yet record that a merge happened.
58 # - Can use a variety of location-identifier formats
61 # - Add "svnmerge avail -R": show logs in reverse order
63 # Information for Hackers:
65 # Identifiers for branches:
66 # A branch is identified in three ways within this source:
67 # - as a working copy (variable name usually includes 'dir')
68 # - as a fully qualified URL
69 # - as a path identifier (an opaque string indicating a particular path
70 # in a particular repository; variable name includes 'pathid')
71 # A "target" is generally user-specified, and may be a working copy or
74 import sys
, os
, getopt
, re
, types
, tempfile
, time
, locale
75 from bisect
import bisect
76 from xml
.dom
import pulldom
79 if not hasattr(sys
, "version_info") or sys
.version_info
< (2, 0):
80 error("requires Python 2.0 or newer")
82 # Set up the separator used to separate individual log messages from
83 # each revision merged into the target location. Also, create a
84 # regular expression that will find this same separator in already
85 # committed log messages, so that the separator used for this run of
86 # svnmerge.py will have one more LOG_SEPARATOR appended to the longest
87 # separator found in all the commits.
88 LOG_SEPARATOR
= 8 * '.'
89 LOG_SEPARATOR_RE
= re
.compile('^((%s)+)' % re
.escape(LOG_SEPARATOR
),
92 # Each line of the embedded log messages will be prefixed by LOG_LINE_PREFIX.
93 LOG_LINE_PREFIX
= 2 * ' '
95 # Set python to the default locale as per environment settings, same as svn
96 # TODO we should really parse config and if log-encoding is specified, set
97 # the locale to match that encoding
98 locale
.setlocale(locale
.LC_ALL
, '')
100 # We want the svn output (such as svn info) to be non-localized
101 # Using LC_MESSAGES should not affect localized output of svn log, for example
102 if os
.environ
.has_key("LC_ALL"):
103 del os
.environ
["LC_ALL"]
104 os
.environ
["LC_MESSAGES"] = "C"
106 ###############################################################################
107 # Support for older Python versions
108 ###############################################################################
110 # True/False constants are Python 2.2+
117 """Replacement for str.lstrip (support for arbitrary chars to strip was
118 added in Python 2.2.2)."""
128 """Replacement for str.rstrip (support for arbitrary chars to strip was
129 added in Python 2.2.2)."""
141 """Replacement for str.strip (support for arbitrary chars to strip was
142 added in Python 2.2.2)."""
143 return lstrip(rstrip(s
, ch
), ch
)
145 def rsplit(s
, sep
, maxsplits
=0):
146 """Like str.rsplit, which is Python 2.4+ only."""
148 if not 0 < maxsplits
<= len(L
):
150 return [sep
.join(L
[0:-maxsplits
])] + L
[-maxsplits
:]
152 ###############################################################################
155 """Extract info from a svn keyword string."""
157 return strip(s
, "$").strip().split(": ")[1]
161 __revision__
= kwextract('$Rev$')
162 __date__
= kwextract('$Date$')
164 # Additional options, not (yet?) mapped to command line flags
167 "prop": NAME
+ "-integrated",
168 "block-prop": NAME
+ "-blocked",
169 "commit-verbose": True,
175 """Get the width of the console screen (if any)."""
177 return int(os
.environ
["COLUMNS"])
178 except (KeyError, ValueError):
182 # Call the Windows API (requires ctypes library)
183 from ctypes
import windll
, create_string_buffer
184 h
= windll
.kernel32
.GetStdHandle(-11)
185 csbi
= create_string_buffer(22)
186 res
= windll
.kernel32
.GetConsoleScreenBufferInfo(h
, csbi
)
191 left
, top
, right
, bottom
,
192 maxx
, maxy
) = struct
.unpack("hhhhHhhhhhh", csbi
.raw
)
193 return right
- left
+ 1
197 # Parse the output of stty -a
199 out
= os
.popen("stty -a").read()
200 m
= re
.search(r
"columns (\d+);", out
)
202 return int(m
.group(1))
208 """Subroutine to output an error and bail."""
209 print >> sys
.stderr
, "%s: %s" % (NAME
, s
)
213 """Subroutine to output progress message, unless in quiet mode."""
215 print "%s: %s" % (NAME
, s
)
217 def prefix_lines(prefix
, lines
):
218 """Given a string representing one or more lines of text, insert the
219 specified prefix at the beginning of each line, and return the result.
220 The input must be terminated by a newline."""
221 assert lines
[-1] == "\n"
222 return prefix
+ lines
[:-1].replace("\n", "\n"+prefix
) + "\n"
224 def recode_stdout_to_file(s
):
225 if locale
.getdefaultlocale()[1] is None or not hasattr(sys
.stdout
, "encoding") \
226 or sys
.stdout
.encoding
is None:
228 u
= s
.decode(sys
.stdout
.encoding
)
229 return u
.encode(locale
.getdefaultlocale()[1])
231 class LaunchError(Exception):
232 """Signal a failure in execution of an external command. Parameters are the
233 exit code of the process, the original command line, and the output of the
237 """Launch a sub-process. Return its output (both stdout and stderr),
238 optionally split by lines (if split_lines is True). Raise a LaunchError
239 exception if the exit code of the process is non-zero (failure).
241 This function has two implementations, one based on subprocess (preferred),
242 and one based on popen (for compatibility).
247 def launch(cmd
, split_lines
=True):
248 # Requiring python 2.4 or higher, on some platforms we get
249 # much faster performance from the subprocess module (where python
250 # doesn't try to close an exhorbitant number of file descriptors)
255 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, \
256 close_fds
=False, stderr
=subprocess
.PIPE
)
258 # Use shlex to break up the parameters intelligently,
259 # respecting quotes. shlex can't handle unicode.
260 args
= shlex
.split(cmd
.encode('ascii'))
261 p
= subprocess
.Popen(args
, stdout
=subprocess
.PIPE
, \
262 close_fds
=False, stderr
=subprocess
.PIPE
)
263 stdoutAndErr
= p
.communicate()
264 stdout
= stdoutAndErr
[0]
265 stderr
= stdoutAndErr
[1]
266 except OSError, inst
:
267 # Using 1 as failure code; should get actual number somehow? For
268 # examples see svnmerge_test.py's TestCase_launch.test_failure and
269 # TestCase_launch.test_failurecode.
270 raise LaunchError(1, cmd
, stdout
+ " " + stderr
+ ": " + str(inst
))
272 if p
.returncode
== 0:
274 # Setting keepends=True for compatibility with previous logic
275 # (where file.readlines() preserves newlines)
276 return stdout
.splitlines(True)
280 raise LaunchError(p
.returncode
, cmd
, stdout
+ stderr
)
282 # support versions of python before 2.4 (slower on some systems)
283 def launch(cmd
, split_lines
=True):
284 if os
.name
not in ['nt', 'os2']:
286 p
= popen2
.Popen4(cmd
)
289 out
= p
.fromchild
.readlines()
291 out
= p
.fromchild
.read()
308 raise LaunchError(ret
, cmd
, out
)
310 def launchsvn(s
, show
=False, pretend
=False, **kwargs
):
311 """Launch SVN and grab its output."""
312 username
= password
= configdir
= ""
313 if opts
.get("username", None):
314 username
= "--username=" + opts
["username"]
315 if opts
.get("password", None):
316 password
= "--password=" + opts
["password"]
317 if opts
.get("config-dir", None):
318 configdir
= "--config-dir=" + opts
["config-dir"]
319 cmd
= ' '.join(filter(None, [opts
["svn"], "--non-interactive",
320 username
, password
, configdir
, s
]))
321 if show
or opts
["verbose"] >= 2:
325 return launch(cmd
, **kwargs
)
328 """Do (or pretend to do) an SVN command."""
329 out
= launchsvn(s
, show
=opts
["show-changes"] or opts
["dry-run"],
330 pretend
=opts
["dry-run"],
332 if not opts
["dry-run"]:
335 def check_dir_clean(dir):
336 """Check the current status of dir for local mods."""
338 report('skipping status check because of --force')
340 report('checking status of "%s"' % dir)
342 # Checking with -q does not show unversioned files or external
343 # directories. Though it displays a debug message for external
344 # directories, after a blank line. So, practically, the first line
345 # matters: if it's non-empty there is a modification.
346 out
= launchsvn("status -q %s" % dir)
347 if out
and out
[0].strip():
348 error('"%s" has local modifications; it must be clean' % dir)
350 class PathIdentifier
:
351 """Abstraction for a path identifier, so that we can start talking
352 about it before we know the form that it takes in the properties (its
353 external_form). Objects are referenced in the class variable 'locobjs',
354 keyed by all known forms."""
356 # a map of UUID (or None) to repository root URL.
359 # a map from any known string form to the corresponding PathIdentifier
362 def __init__(self
, repo_relative_path
, uuid
=None, url
=None, external_form
=None):
363 self
.repo_relative_path
= repo_relative_path
366 self
.external_form
= external_form
369 return "<PathIdentifier " + ', '.join('%s=%r' % i
for i
in self
.__dict
__.items()) + '>'
372 """Return a printable string representation"""
373 if self
.external_form
:
374 return self
.external_form
376 return self
.format('url')
378 return self
.format('uuid')
379 return self
.format('path')
381 def from_pathid(pathid_str
):
382 """convert pathid_str to a PathIdentifier"""
383 if not PathIdentifier
.locobjs
.has_key(pathid_str
):
384 if is_url(pathid_str
):
385 # we can determine every form; PathIdentifier.hint knows how to do that
386 PathIdentifier
.hint(pathid_str
)
387 elif pathid_str
[:7] == 'uuid://':
388 mo
= re
.match('uuid://([^/]*)(.*)', pathid_str
)
390 error("Invalid path identifier '%s'" % pathid_str
)
391 uuid
, repo_relative_path
= mo
.groups()
392 pathid
= PathIdentifier(repo_relative_path
, uuid
=uuid
)
393 # we can cache this by uuid:// pathid and by repo-relative path
394 PathIdentifier
.locobjs
[pathid_str
] = PathIdentifier
.locobjs
[repo_relative_path
] = pathid
395 elif pathid_str
and pathid_str
[0] == '/':
396 # strip any trailing slashes
397 pathid_str
= pathid_str
.rstrip('/')
398 pathid
= PathIdentifier(repo_relative_path
=pathid_str
)
399 # we can only cache this by repo-relative path
400 PathIdentifier
.locobjs
[pathid_str
] = pathid
402 error("Invalid path identifier '%s'" % pathid_str
)
403 return PathIdentifier
.locobjs
[pathid_str
]
404 from_pathid
= staticmethod(from_pathid
)
406 def from_target(target
):
407 """Convert a target (either a working copy path or an URL) into a
409 # prime the cache first if we don't know about this target yet
410 if not PathIdentifier
.locobjs
.has_key(target
):
411 PathIdentifier
.hint(target
)
414 return PathIdentifier
.locobjs
[target
]
416 error("Could not recognize path identifier '%s'" % target
)
417 from_target
= staticmethod(from_target
)
420 """Cache some information about target, as it may be referenced by
421 repo-relative path in subversion properties; the cache can help to
422 expand such a relative path to a full path identifier."""
423 if PathIdentifier
.locobjs
.has_key(target
): return
424 if not is_url(target
) and not is_wc(target
): return
426 url
= target_to_url(target
)
428 root
= get_repo_root(url
)
429 assert root
[-1] != "/"
430 assert url
[:len(root
)] == root
, "url=%r, root=%r" % (url
, root
)
431 repo_relative_path
= url
[len(root
):]
434 uuid
= get_svninfo(target
)['Repository UUID']
435 uuid_pathid
= 'uuid://%s%s' % (uuid
, repo_relative_path
)
440 locobj
= PathIdentifier
.locobjs
.get(url
) or \
441 (uuid_pathid
and PathIdentifier
.locobjs
.get(uuid_pathid
))
443 locobj
= PathIdentifier(repo_relative_path
, uuid
=uuid
, url
=url
)
445 PathIdentifier
.repo_hints
[uuid
] = root
# (uuid may be None)
447 PathIdentifier
.locobjs
[target
] = locobj
448 PathIdentifier
.locobjs
[url
] = locobj
450 PathIdentifier
.locobjs
[uuid_pathid
] = locobj
451 if not PathIdentifier
.locobjs
.has_key(repo_relative_path
):
452 PathIdentifier
.locobjs
[repo_relative_path
] = locobj
453 hint
= staticmethod(hint
)
455 def format(self
, fmt
):
457 return self
.repo_relative_path
459 return "uuid://%s%s" % (self
.uuid
, self
.repo_relative_path
)
463 error("Unkonwn path type '%s'" % fmt
)
465 def match_substring(self
, str):
466 """Test whether str is a substring of any representation of this
468 if self
.repo_relative_path
.find(str) >= 0:
472 if ("uuid://%s%s" % (self
.uuid
, self
.repo_relative_path
)).find(str) >= 0:
476 if (self
.url
+ self
.repo_relative_path
).find(str) >= 0:
482 """Convert a pathid into a URL. If this is not possible, error out."""
485 # if we have a uuid and happen to know the URL for it, use that
486 elif self
.uuid
and PathIdentifier
.repo_hints
.has_key(self
.uuid
):
487 self
.url
= PathIdentifier
.repo_hints
[self
.uuid
] + self
.repo_relative_path
488 PathIdentifier
.locobjs
[self
.url
] = self
490 # if we've only seen one rep, use that (a guess, but an educated one)
491 elif not self
.uuid
and len(PathIdentifier
.repo_hints
) == 1:
492 uuid
, root
= PathIdentifier
.repo_hints
.items()[0]
495 PathIdentifier
.locobjs
['uuid://%s%s' % (uuid
, self
.repo_relative_path
)] = self
496 self
.url
= root
+ self
.repo_relative_path
497 PathIdentifier
.locobjs
[self
.url
] = self
498 report("Guessing that '%s' refers to '%s'" % (self
, self
.url
))
501 error("Cannot determine URL for '%s'; " % self
+
502 "Explicit source argument (-S/--source) required.\n")
506 A log of the revisions which affected a given URL between two
510 def __init__(self
, url
, begin
, end
, find_propchanges
=False):
512 Create a new RevisionLog object, which stores, in self.revs, a list
513 of the revisions which affected the specified URL between begin and
514 end. If find_propchanges is True, self.propchange_revs will contain a
515 list of the revisions which changed properties directly on the
516 specified URL. URL must be the URL for a directory in the repository.
520 # Setup the log options (--quiet, so we don't show log messages)
521 log_opts
= '--xml --quiet -r%s:%s "%s"' % (begin
, end
, url
)
523 # The --verbose flag lets us grab merge tracking information
524 # by looking at propchanges
525 log_opts
= "--verbose " + log_opts
527 # Read the log to look for revision numbers and merge-tracking info
529 self
.propchange_revs
= []
530 repos_pathid
= PathIdentifier
.from_target(url
)
531 for chg
in SvnLogParser(launchsvn("log %s" % log_opts
,
533 self
.revs
.append(chg
.revision())
534 for p
in chg
.paths():
535 if p
.action() == 'M' and p
.pathid() == repos_pathid
.repo_relative_path
:
536 self
.propchange_revs
.append(chg
.revision())
538 # Save the range of the log
539 self
.begin
= int(begin
)
541 # If end is not provided, we do not know which is the latest
542 # revision in the repository. So we set 'end' to the latest
544 self
.end
= self
.revs
[-1]
551 def merge_metadata(self
):
553 Return a VersionedProperty object, with a cached view of the merge
554 metadata in the range of this log.
557 # Load merge metadata if necessary
559 self
._merges
= VersionedProperty(self
.url
, opts
["prop"])
560 self
._merges
.load(self
)
564 def block_metadata(self
):
566 self
._blocks
= VersionedProperty(self
.url
, opts
["block-prop"])
567 self
._blocks
.load(self
)
572 class VersionedProperty
:
574 A read-only, cached view of a versioned property.
576 self.revs contains a list of the revisions in which the property changes.
577 self.values stores the new values at each corresponding revision. If the
578 value of the property is unknown, it is set to None.
580 Initially, we set self.revs to [0] and self.values to [None]. This
581 indicates that, as of revision zero, we know nothing about the value of
584 Later, if you run self.load(log), we cache the value of this property over
585 the entire range of the log by noting each revision in which the property
586 was changed. At the end of the range of the log, we invalidate our cache
587 by adding the value "None" to our cache for any revisions which fall out
588 of the range of our log.
590 Once self.revs and self.values are filled, we can find the value of the
591 property at any arbitrary revision using a binary search on self.revs.
592 Once we find the last revision during which the property was changed,
593 we can lookup the associated value in self.values. (If the associated
594 value is None, the associated value was not cached and we have to do
597 An example: We know that the 'svnmerge' property was added in r10, and
598 changed in r21. We gathered log info up until r40.
600 revs = [0, 10, 21, 40]
601 values = [None, "val1", "val2", None]
603 What these values say:
604 - From r0 to r9, we know nothing about the property.
605 - In r10, the property was set to "val1". This property stayed the same
606 until r21, when it was changed to "val2".
607 - We don't know what happened after r40.
610 def __init__(self
, url
, name
):
611 """View the history of a versioned property at URL with name"""
615 # We know nothing about the value of the property. Setup revs
616 # and values to indicate as such.
620 # We don't have any revisions cached
621 self
._initial
_value
= None
622 self
._changed
_revs
= []
623 self
._changed
_values
= []
627 Load the history of property changes from the specified
631 # Get the property value before the range of the log
633 self
.revs
.append(log
.begin
-1)
635 self
._initial
_value
= self
.raw_get(log
.begin
-1)
637 # The specified URL might not exist before the
638 # range of the log. If so, we can safely assume
639 # that the property was empty at that time.
640 self
._initial
_value
= { }
641 self
.values
.append(self
._initial
_value
)
643 self
._initial
_value
= { }
644 self
.values
[0] = self
._initial
_value
646 # Cache the property values in the log range
647 old_value
= self
._initial
_value
648 for rev
in log
.propchange_revs
:
649 new_value
= self
.raw_get(rev
)
650 if new_value
!= old_value
:
651 self
._changed
_revs
.append(rev
)
652 self
._changed
_values
.append(new_value
)
653 self
.revs
.append(rev
)
654 self
.values
.append(new_value
)
655 old_value
= new_value
657 # Indicate that we know nothing about the value of the property
658 # after the range of the log.
660 self
.revs
.append(log
.end
+1)
661 self
.values
.append(None)
663 def raw_get(self
, rev
=None):
665 Get the property at revision REV. If rev is not specified, get
666 the property at revision HEAD.
668 return get_revlist_prop(self
.url
, self
.name
, rev
)
670 def get(self
, rev
=None):
672 Get the property at revision REV. If rev is not specified, get
673 the property at revision HEAD.
678 # Find the index using a binary search
679 i
= bisect(self
.revs
, rev
) - 1
681 # Return the value of the property, if it was cached
682 if self
.values
[i
] is not None:
683 return self
.values
[i
]
685 # Get the current value of the property
686 return self
.raw_get(rev
)
688 def changed_revs(self
, key
=None):
690 Get a list of the revisions in which the specified dictionary
691 key was changed in this property. If key is not specified,
692 return a list of revisions in which any key was changed.
695 return self
._changed
_revs
698 old_val
= self
._initial
_value
699 for rev
, val
in zip(self
._changed
_revs
, self
._changed
_values
):
700 if val
.get(key
) != old_val
.get(key
):
701 changed_revs
.append(rev
)
705 def initialized_revs(self
):
707 Get a list of the revisions in which keys were added or
708 removed in this property.
710 initialized_revs
= []
711 old_len
= len(self
._initial
_value
)
712 for rev
, val
in zip(self
._changed
_revs
, self
._changed
_values
):
713 if len(val
) != old_len
:
714 initialized_revs
.append(rev
)
716 return initialized_revs
720 A set of revisions, held in dictionary form for easy manipulation. If we
721 were to rewrite this script for Python 2.3+, we would subclass this from
722 set (or UserSet). As this class does not include branch
723 information, it's assumed that one instance will be used per
726 def __init__(self
, parm
):
727 """Constructs a RevisionSet from a string in property form, or from
728 a dictionary whose keys are the revisions. Raises ValueError if the
729 input string is invalid."""
733 revision_range_split_re
= re
.compile('[-:]')
735 if isinstance(parm
, types
.DictType
):
736 self
._revs
= parm
.copy()
737 elif isinstance(parm
, types
.ListType
):
739 self
._revs
[int(R
)] = 1
743 for R
in parm
.split(","):
744 rev_or_revs
= re
.split(revision_range_split_re
, R
)
745 if len(rev_or_revs
) == 1:
746 self
._revs
[int(rev_or_revs
[0])] = 1
747 elif len(rev_or_revs
) == 2:
748 for rev
in range(int(rev_or_revs
[0]),
749 int(rev_or_revs
[1])+1):
752 raise ValueError, 'Ill formatted revision range: ' + R
755 revnums
= self
._revs
.keys()
759 def normalized(self
):
760 """Returns a normalized version of the revision set, which is an
761 ordered list of couples (start,end), with the minimum number of
763 revnums
= self
.sorted()
767 s
= e
= revnums
.pop()
768 while revnums
and revnums
[-1] in (e
, e
+1):
774 """Convert the revision set to a string, using its normalized form."""
776 for s
,e
in self
.normalized():
780 L
.append(str(s
) + "-" + str(e
))
783 def __contains__(self
, rev
):
784 return self
._revs
.has_key(rev
)
786 def __sub__(self
, rs
):
787 """Compute subtraction as in sets."""
789 for r
in self
._revs
.keys():
792 return RevisionSet(revs
)
794 def __and__(self
, rs
):
795 """Compute intersections as in sets."""
797 for r
in self
._revs
.keys():
800 return RevisionSet(revs
)
802 def __nonzero__(self
):
803 return len(self
._revs
) != 0
806 """Return the number of revisions in the set."""
807 return len(self
._revs
)
810 return iter(self
.sorted())
812 def __or__(self
, rs
):
813 """Compute set union."""
814 revs
= self
._revs
.copy()
815 revs
.update(rs
._revs
)
816 return RevisionSet(revs
)
818 def merge_props_to_revision_set(merge_props
, pathid
):
819 """A converter which returns a RevisionSet instance containing the
820 revisions from PATH as known to BRANCH_PROPS. BRANCH_PROPS is a
821 dictionary of pathid -> revision set branch integration information
822 (as returned by get_merge_props())."""
823 if not merge_props
.has_key(pathid
):
824 error('no integration info available for path "%s"' % pathid
)
825 return RevisionSet(merge_props
[pathid
])
827 def dict_from_revlist_prop(propvalue
):
828 """Given a property value as a string containing per-source revision
829 lists, return a dictionary whose key is a source path identifier
830 and whose value is the revisions for that source."""
833 # Multiple sources are separated by any whitespace.
834 for L
in propvalue
.split():
835 # We use rsplit to play safe and allow colons in pathids.
836 pathid_str
, revs
= rsplit(L
.strip(), ":", 1)
838 pathid
= PathIdentifier
.from_pathid(pathid_str
)
840 # cache the "external" form we saw
841 pathid
.external_form
= pathid_str
846 def get_revlist_prop(url_or_dir
, propname
, rev
=None):
847 """Given a repository URL or working copy path and a property
848 name, extract the values of the property which store per-source
849 revision lists and return a dictionary whose key is a source path
850 identifier, and whose value is the revisions for that source."""
852 # Note that propget does not return an error if the property does
853 # not exist, it simply does not output anything. So we do not need
854 # to check for LaunchError here.
855 args
= '--strict "%s" "%s"' % (propname
, url_or_dir
)
857 args
= '-r %s %s' % (rev
, args
)
858 out
= launchsvn('propget %s' % args
, split_lines
=False)
860 return dict_from_revlist_prop(out
)
862 def get_merge_props(dir):
863 """Extract the merged revisions."""
864 return get_revlist_prop(dir, opts
["prop"])
866 def get_block_props(dir):
867 """Extract the blocked revisions."""
868 return get_revlist_prop(dir, opts
["block-prop"])
870 def get_blocked_revs(dir, source_pathid
):
871 p
= get_block_props(dir)
872 if p
.has_key(source_pathid
):
873 return RevisionSet(p
[source_pathid
])
874 return RevisionSet("")
876 def format_merge_props(props
, sep
=" "):
877 """Formats the hash PROPS as a string suitable for use as a
878 Subversion property value."""
879 assert sep
in ["\t", "\n", " "] # must be a whitespace
880 props
= props
.items()
884 L
.append("%s:%s" % (h
, r
))
887 def _run_propset(dir, prop
, value
):
888 """Set the property 'prop' of directory 'dir' to value 'value'. We go
889 through a temporary file to not run into command line length limits."""
891 fd
, fname
= tempfile
.mkstemp()
892 f
= os
.fdopen(fd
, "wb")
893 except AttributeError:
894 # Fallback for Python <= 2.3 which does not have mkstemp (mktemp
895 # suffers from race conditions. Not that we care...)
896 fname
= tempfile
.mktemp()
897 f
= open(fname
, "wb")
902 report("property data written to temp file: %s" % value
)
903 svn_command('propset "%s" -F "%s" "%s"' % (prop
, fname
, dir))
907 def set_props(dir, name
, props
):
908 props
= format_merge_props(props
)
910 _run_propset(dir, name
, props
)
912 # Check if NAME exists on DIR before trying to delete it.
913 # As of 1.6 propdel no longer supports deleting a
914 # non-existent property.
915 out
= launchsvn('propget "%s" "%s"' % (name
, dir))
917 svn_command('propdel "%s" "%s"' % (name
, dir))
919 def set_merge_props(dir, props
):
920 set_props(dir, opts
["prop"], props
)
922 def set_block_props(dir, props
):
923 set_props(dir, opts
["block-prop"], props
)
925 def set_blocked_revs(dir, source_pathid
, revs
):
926 props
= get_block_props(dir)
928 props
[source_pathid
] = str(revs
)
929 elif props
.has_key(source_pathid
):
930 del props
[source_pathid
]
931 set_block_props(dir, props
)
934 """Check if url looks like a valid url."""
935 return re
.search(r
"^[a-zA-Z][-+\.\w]*://[^\s]+$", url
) is not None and url
[:4] != 'uuid'
938 """Similar to is_url, but actually invoke get_svninfo to find out"""
939 return get_svninfo(url
) != {}
941 def is_pathid(pathid
):
942 return isinstance(pathid
, PathIdentifier
)
945 """Check if a directory is a working copy."""
946 return os
.path
.isdir(os
.path
.join(dir, ".svn")) or \
947 os
.path
.isdir(os
.path
.join(dir, "_svn"))
950 def get_svninfo(target
):
951 """Extract the subversion information for a target (through 'svn info').
952 This function uses an internal cache to let clients query information
954 if _cache_svninfo
.has_key(target
):
955 return _cache_svninfo
[target
]
957 for L
in launchsvn('info "%s"' % target
):
961 key
, value
= L
.split(": ", 1)
962 info
[key
] = value
.strip()
963 _cache_svninfo
[target
] = info
966 def target_to_url(target
):
967 """Convert working copy path or repos URL to a repos URL."""
969 info
= get_svninfo(target
)
974 def get_repo_root(target
):
975 """Compute the root repos URL given a working-copy path, or a URL."""
976 # Try using "svn info WCDIR". This works only on SVN clients >= 1.3
977 if not is_url(target
):
979 info
= get_svninfo(target
)
980 root
= info
["Repository Root"]
981 _cache_reporoot
[root
] = None
985 url
= target_to_url(target
)
986 assert url
[-1] != '/'
990 # Go through the cache of the repository roots. This avoids extra
991 # server round-trips if we are asking the root of different URLs
992 # in the same repository (the cache in get_svninfo() cannot detect
993 # that of course and would issue a remote command).
995 for r
in _cache_reporoot
:
996 if url
.startswith(r
):
999 # Try using "svn info URL". This works only on SVN clients >= 1.2
1001 info
= get_svninfo(url
)
1002 # info may be {}, in which case we'll see KeyError here
1003 root
= info
["Repository Root"]
1004 _cache_reporoot
[root
] = None
1006 except (KeyError, LaunchError
):
1009 # Constrained to older svn clients, we are stuck with this ugly
1010 # trial-and-error implementation. It could be made faster with a
1013 temp
= os
.path
.dirname(url
)
1015 launchsvn('proplist "%s"' % temp
)
1017 _cache_reporoot
[url
] = None
1018 return rstrip(url
, "/")
1021 error("svn repos root of %s not found" % target
)
1025 Parse the "svn log", going through the XML output and using pulldom (which
1026 would even allow streaming the command output).
1028 def __init__(self
, xml
):
1029 self
._events
= pulldom
.parseString(xml
)
1030 def __getitem__(self
, idx
):
1031 for event
, node
in self
._events
:
1032 if event
== pulldom
.START_ELEMENT
and node
.tagName
== "logentry":
1033 self
._events
.expandNode(node
)
1034 return self
.SvnLogRevision(node
)
1035 raise IndexError, "Could not find 'logentry' tag in xml"
1037 class SvnLogRevision
:
1038 def __init__(self
, xmlnode
):
1041 return int(self
.n
.getAttribute("revision"))
1043 return self
.n
.getElementsByTagName("author")[0].firstChild
.data
1045 return [self
.SvnLogPath(n
)
1046 for n
in self
.n
.getElementsByTagName("path")]
1049 def __init__(self
, xmlnode
):
1052 return self
.n
.getAttribute("action")
1054 return self
.n
.firstChild
.data
1055 def copyfrom_rev(self
):
1056 try: return self
.n
.getAttribute("copyfrom-rev")
1057 except KeyError: return None
1058 def copyfrom_pathid(self
):
1059 try: return self
.n
.getAttribute("copyfrom-path")
1060 except KeyError: return None
1062 def get_copyfrom(target
):
1063 """Get copyfrom info for a given target (it represents the
1064 repository-relative path from where it was branched). NOTE:
1065 repos root has no copyfrom info. In this case None is returned.
1068 - source file or directory from which the copy was made
1069 - revision from which that source was copied
1070 - revision in which the copy was committed
1072 repos_path
= PathIdentifier
.from_target(target
).repo_relative_path
1073 for chg
in SvnLogParser(launchsvn('log -v --xml --stop-on-copy "%s"'
1074 % target
, split_lines
=False)):
1075 for p
in chg
.paths():
1076 if p
.action() == 'A' and p
.pathid() == repos_path
:
1077 # These values will be None if the corresponding elements are
1078 # not found in the log.
1079 return p
.copyfrom_pathid(), p
.copyfrom_rev(), chg
.revision()
1080 return None,None,None
1082 def get_latest_rev(url
):
1083 """Get the latest revision of the repository of which URL is part."""
1085 info
= get_svninfo(url
)
1086 if not info
.has_key("Revision"):
1087 error("Not a valid URL: %s" % url
)
1088 return info
["Revision"]
1090 # Alternative method for latest revision checking (for svn < 1.2)
1091 report('checking latest revision of "%s"' % url
)
1092 L
= launchsvn('proplist --revprop -r HEAD "%s"' % opts
["source-url"])[0]
1093 rev
= re
.search("revision (\d+)", L
).group(1)
1094 report('latest revision of "%s" is %s' % (url
, rev
))
1097 def get_created_rev(url
):
1098 """Lookup the revision at which the path identified by the
1099 provided URL was first created."""
1101 report('determining oldest revision for URL "%s"' % url
)
1102 ### TODO: Refactor this to use a modified RevisionLog class.
1104 cmd
= "log -r1:HEAD --stop-on-copy -q " + url
1106 lines
= launchsvn(cmd
+ " --limit=1")
1108 # Assume that --limit isn't supported by the installed 'svn'.
1109 lines
= launchsvn(cmd
)
1110 if lines
and len(lines
) > 1:
1111 i
= lines
[1].find(" ")
1113 oldest_rev
= int(lines
[1][1:i
])
1114 if oldest_rev
== -1:
1115 error('unable to determine oldest revision for URL "%s"' % url
)
1118 def get_commit_log(url
, revnum
):
1119 """Return the log message for a specific integer revision
1121 out
= launchsvn("log --incremental -r%d %s" % (revnum
, url
))
1122 return recode_stdout_to_file("".join(out
[1:]))
1124 def construct_merged_log_message(url
, revnums
):
1125 """Return a commit log message containing all the commit messages
1126 in the specified revisions at the given URL. The separator used
1127 in this log message is determined by searching for the longest
1128 svnmerge separator existing in the commit log messages and
1129 extending it by one more separator. This results in a new commit
1130 log message that is clearer in describing merges that contain
1131 other merges. Trailing newlines are removed from the embedded
1135 for r
in revnums
.sorted():
1136 message
= get_commit_log(url
, r
)
1138 message
= re
.sub(r
'(\r\n|\r|\n)', "\n", message
)
1139 message
= rstrip(message
, "\n") + "\n"
1140 messages
.append(prefix_lines(LOG_LINE_PREFIX
, message
))
1141 for match
in LOG_SEPARATOR_RE
.findall(message
):
1143 if len(sep
) > len(longest_sep
):
1146 longest_sep
+= LOG_SEPARATOR
+ "\n"
1148 return longest_sep
.join(messages
)
1150 def get_default_source(branch_target
, branch_props
):
1151 """Return the default source for branch_target (given its branch_props).
1152 Error out if there is ambiguity."""
1153 if not branch_props
:
1154 error("no integration info available")
1156 props
= branch_props
.copy()
1157 pathid
= PathIdentifier
.from_target(branch_target
)
1159 # To make bidirectional merges easier, find the target's
1160 # repository local path so it can be removed from the list of
1161 # possible integration sources.
1162 if props
.has_key(pathid
):
1166 err_msg
= "multiple sources found. "
1167 err_msg
+= "Explicit source argument (-S/--source) required.\n"
1168 err_msg
+= "The merge sources available are:"
1170 err_msg
+= "\n " + str(prop
)
1173 return props
.keys()[0]
1175 def should_find_reflected(branch_dir
):
1176 should_find_reflected
= opts
["bidirectional"]
1178 # If the source has integration info for the target, set find_reflected
1179 # even if --bidirectional wasn't specified
1180 if not should_find_reflected
:
1181 source_props
= get_merge_props(opts
["source-url"])
1182 should_find_reflected
= source_props
.has_key(PathIdentifier
.from_target(branch_dir
))
1184 return should_find_reflected
1186 def analyze_revs(target_pathid
, url
, begin
=1, end
=None,
1187 find_reflected
=False):
1188 """For the source of the merges in the source URL being merged into
1189 target_pathid, analyze the revisions in the interval begin-end (which
1190 defaults to 1-HEAD), to find out which revisions are changes in
1191 the url, which are changes elsewhere (so-called 'phantom'
1192 revisions), optionally which are reflected changes (to avoid
1193 conflicts that can occur when doing bidirectional merging between
1194 branches), and which revisions initialize merge tracking against other
1195 branches. Return a tuple of four RevisionSet's:
1196 (real_revs, phantom_revs, reflected_revs, initialized_revs).
1198 NOTE: To maximize speed, if "end" is not provided, the function is
1199 not able to find phantom revisions following the last real
1200 revision in the URL.
1208 if long(begin
) > long(end
):
1209 return RevisionSet(""), RevisionSet(""), \
1210 RevisionSet(""), RevisionSet("")
1212 logs
[url
] = RevisionLog(url
, begin
, end
, find_reflected
)
1213 revs
= RevisionSet(logs
[url
].revs
)
1216 # If end is not provided, we do not know which is the latest revision
1217 # in the repository. So return the phantom revision set only up to
1218 # the latest known revision.
1219 end
= str(list(revs
)[-1])
1221 phantom_revs
= RevisionSet("%s-%s" % (begin
, end
)) - revs
1224 reflected_revs
= logs
[url
].merge_metadata().changed_revs(target_pathid
)
1225 reflected_revs
+= logs
[url
].block_metadata().changed_revs(target_pathid
)
1229 initialized_revs
= RevisionSet(logs
[url
].merge_metadata().initialized_revs())
1230 reflected_revs
= RevisionSet(reflected_revs
)
1232 return revs
, phantom_revs
, reflected_revs
, initialized_revs
1234 def analyze_source_revs(branch_target
, source_url
, **kwargs
):
1235 """For the given branch and source, extract the real and phantom
1236 source revisions."""
1237 branch_url
= target_to_url(branch_target
)
1238 branch_pathid
= PathIdentifier
.from_target(branch_target
)
1240 # Extract the latest repository revision from the URL of the branch
1241 # directory (which is already cached at this point).
1242 end_rev
= get_latest_rev(source_url
)
1244 # Calculate the base of analysis. If there is a "1-XX" interval in the
1245 # merged_revs, we do not need to check those.
1247 r
= opts
["merged-revs"].normalized()
1248 if r
and r
[0][0] == 1:
1251 # See if the user filtered the revision set. If so, we are not
1252 # interested in something outside that range.
1253 if opts
["revision"]:
1254 revs
= RevisionSet(opts
["revision"]).sorted()
1257 if end_rev
> revs
[-1]:
1260 return analyze_revs(branch_pathid
, source_url
, base
, end_rev
, **kwargs
)
1262 def minimal_merge_intervals(revs
, phantom_revs
):
1263 """Produce the smallest number of intervals suitable for merging. revs
1264 is the RevisionSet which we want to merge, and phantom_revs are phantom
1265 revisions which can be used to concatenate intervals, thus minimizing the
1266 number of operations."""
1267 revnums
= revs
.normalized()
1272 next
= revnums
.pop()
1273 assert next
[1] < cur
[0] # otherwise it is not ordered
1274 assert cur
[0] - next
[1] > 1 # otherwise it is not normalized
1275 for i
in range(next
[1]+1, cur
[0]):
1276 if i
not in phantom_revs
:
1281 cur
= (next
[0], cur
[1])
1287 def display_revisions(revs
, display_style
, revisions_msg
, source_url
):
1288 """Show REVS as dictated by DISPLAY_STYLE, either numerically, in
1289 log format, or as diffs. When displaying revisions numerically,
1290 prefix output with REVISIONS_MSG when in verbose mode. Otherwise,
1291 request logs or diffs using SOURCE_URL."""
1292 if display_style
== "revisions":
1294 report(revisions_msg
)
1296 elif display_style
== "logs":
1297 for start
,end
in revs
.normalized():
1298 svn_command('log --incremental -v -r %d:%d %s' % \
1299 (start
, end
, source_url
))
1300 elif display_style
in ("diffs", "summarize"):
1301 if display_style
== 'summarize':
1302 summarize
= '--summarize '
1306 for start
, end
in revs
.normalized():
1309 print "%s: changes in revision %d follow" % (NAME
, start
)
1311 print "%s: changes in revisions %d-%d follow" % (NAME
,
1315 # Note: the starting revision number to 'svn diff' is
1316 # NOT inclusive so we have to subtract one from ${START}.
1317 svn_command("diff -r %d:%d %s %s" % (start
- 1, end
, summarize
,
1320 assert False, "unhandled display style: %s" % display_style
1322 def action_init(target_dir
, target_props
):
1323 """Initialize for merges."""
1324 # Check that directory is ready for being modified
1325 check_dir_clean(target_dir
)
1327 target_pathid
= PathIdentifier
.from_target(target_dir
)
1328 source_pathid
= opts
['source-pathid']
1329 if source_pathid
== target_pathid
:
1330 error("cannot init integration source path '%s'\nIts path identifier does not "
1331 "differ from the path identifier of the current directory, '%s'."
1332 % (source_pathid
, target_pathid
))
1334 source_url
= opts
['source-url']
1336 # If the user hasn't specified the revisions to use, see if the
1337 # "source" is a copy from the current tree and if so, we can use
1338 # the version data obtained from it.
1339 revision_range
= opts
["revision"]
1340 if not revision_range
:
1341 # If source was originally copied from target, and we are merging
1342 # changes from source to target (the copy target is the merge source,
1343 # and the copy source is the merge target), then we want to mark as
1344 # integrated up to the rev in which the copy was committed which
1345 # created the merge source:
1346 cf_source
, cf_rev
, copy_committed_in_rev
= get_copyfrom(source_url
)
1350 cf_url
= get_repo_root(source_url
) + cf_source
1351 if is_url(cf_url
) and check_url(cf_url
):
1352 cf_pathid
= PathIdentifier
.from_target(cf_url
)
1354 if target_pathid
== cf_pathid
:
1355 report('the source "%s" was copied from "%s" in rev %s and committed in rev %s' %
1356 (source_url
, target_dir
, cf_rev
, copy_committed_in_rev
))
1357 revision_range
= "1-" + str(copy_committed_in_rev
)
1359 if not revision_range
:
1360 # If the reverse is true: copy source is the merge source, and
1361 # the copy target is the merge target, then we want to mark as
1362 # integrated up to the specific rev of the merge target from
1363 # which the merge source was copied. (Longer discussion at:
1364 # http://subversion.tigris.org/issues/show_bug.cgi?id=2810 )
1365 cf_source
, cf_rev
, copy_committed_in_rev
= get_copyfrom(target_dir
)
1369 cf_url
= get_repo_root(target_dir
) + cf_source
1370 if is_url(cf_url
) and check_url(cf_url
):
1371 cf_pathid
= PathIdentifier
.from_target(cf_url
)
1373 source_pathid
= PathIdentifier
.from_target(source_url
)
1374 if source_pathid
== cf_pathid
:
1375 report('the target "%s" was copied the source "%s" in rev %s and committed in rev %s' %
1376 (target_dir
, source_url
, cf_rev
, copy_committed_in_rev
))
1377 revision_range
= "1-" + cf_rev
1379 # When neither the merge source nor target is a copy of the other, and
1380 # the user did not specify a revision range, then choose a default which is
1381 # the current revision; saying, in effect, "everything has been merged, so
1382 # mark as integrated up to the latest rev on source url).
1383 if not revision_range
:
1384 revision_range
= "1-" + get_latest_rev(source_url
)
1386 revs
= RevisionSet(revision_range
)
1388 report('marking "%s" as already containing revisions "%s" of "%s"' %
1389 (target_dir
, revs
, source_url
))
1392 # If the local svnmerge-integrated property already has an entry
1393 # for the source-pathid, simply error out.
1394 if not opts
["force"] and target_props
.has_key(source_pathid
):
1395 error('Repository-relative path %s has already been initialized at %s\n'
1396 'Use --force to re-initialize' % (source_pathid
, target_dir
))
1397 # set the pathid's external_form based on the user's options
1398 source_pathid
.external_form
= source_pathid
.format(opts
['location-type'])
1401 target_props
[source_pathid
] = revs
1404 set_merge_props(target_dir
, target_props
)
1406 # Write out commit message if desired
1407 if opts
["commit-file"]:
1408 f
= open(opts
["commit-file"], "w")
1409 print >>f
, 'Initialized merge tracking via "%s" with revisions "%s" from ' \
1411 print >>f
, '%s' % source_url
1413 report('wrote commit message to "%s"' % opts
["commit-file"])
1415 def action_avail(branch_dir
, branch_props
):
1416 """Show commits available for merges."""
1417 source_revs
, phantom_revs
, reflected_revs
, initialized_revs
= \
1418 analyze_source_revs(branch_dir
, opts
["source-url"],
1420 should_find_reflected(branch_dir
))
1421 report('skipping phantom revisions: %s' % phantom_revs
)
1423 report('skipping reflected revisions: %s' % reflected_revs
)
1424 report('skipping initialized revisions: %s' % initialized_revs
)
1426 blocked_revs
= get_blocked_revs(branch_dir
, opts
["source-pathid"])
1427 avail_revs
= source_revs
- opts
["merged-revs"] - blocked_revs
- \
1428 reflected_revs
- initialized_revs
1430 # Compose the set of revisions to show
1431 revs
= RevisionSet("")
1432 report_msg
= "revisions available to be merged are:"
1433 if "avail" in opts
["avail-showwhat"]:
1435 if "blocked" in opts
["avail-showwhat"]:
1436 revs |
= blocked_revs
1437 report_msg
= "revisions blocked are:"
1439 # Limit to revisions specified by -r (if any)
1440 if opts
["revision"]:
1441 revs
= revs
& RevisionSet(opts
["revision"])
1443 display_revisions(revs
, opts
["avail-display"],
1447 def action_integrated(branch_dir
, branch_props
):
1448 """Show change sets already merged. This set of revisions is
1449 calculated from taking svnmerge-integrated property from the
1450 branch, and subtracting any revision older than the branch
1451 creation revision."""
1452 # Extract the integration info for the branch_dir
1453 branch_props
= get_merge_props(branch_dir
)
1454 revs
= merge_props_to_revision_set(branch_props
, opts
["source-pathid"])
1456 # Lookup the oldest revision on the branch path.
1457 oldest_src_rev
= get_created_rev(opts
["source-url"])
1459 # Subtract any revisions which pre-date the branch.
1460 report("subtracting revisions which pre-date the source URL (%d)" %
1462 revs
= revs
- RevisionSet(range(1, oldest_src_rev
))
1464 # Limit to revisions specified by -r (if any)
1465 if opts
["revision"]:
1466 revs
= revs
& RevisionSet(opts
["revision"])
1468 display_revisions(revs
, opts
["integrated-display"],
1469 "revisions already integrated are:", opts
["source-url"])
1471 def action_merge(branch_dir
, branch_props
):
1472 """Record merge meta data, and do the actual merge (if not
1473 requested otherwise via --record-only)."""
1474 # Check branch directory is ready for being modified
1475 check_dir_clean(branch_dir
)
1477 source_revs
, phantom_revs
, reflected_revs
, initialized_revs
= \
1478 analyze_source_revs(branch_dir
, opts
["source-url"],
1480 should_find_reflected(branch_dir
))
1482 if opts
["revision"]:
1483 revs
= RevisionSet(opts
["revision"])
1487 blocked_revs
= get_blocked_revs(branch_dir
, opts
["source-pathid"])
1488 merged_revs
= opts
["merged-revs"]
1490 # Show what we're doing
1491 if opts
["verbose"]: # just to avoid useless calculations
1492 if merged_revs
& revs
:
1493 report('"%s" already contains revisions %s' % (branch_dir
,
1494 merged_revs
& revs
))
1496 report('memorizing phantom revision(s): %s' % phantom_revs
)
1498 report('memorizing reflected revision(s): %s' % reflected_revs
)
1499 if blocked_revs
& revs
:
1500 report('skipping blocked revisions(s): %s' % (blocked_revs
& revs
))
1501 if initialized_revs
:
1502 report('skipping initialized revision(s): %s' % initialized_revs
)
1504 # Compute final merge set.
1505 revs
= revs
- merged_revs
- blocked_revs
- reflected_revs
- \
1506 phantom_revs
- initialized_revs
1508 report('no revisions to merge, exiting')
1511 # When manually marking revisions as merged, we only update the
1512 # integration meta data, and don't perform an actual merge.
1513 record_only
= opts
["record-only"]
1516 report('recording merge of revision(s) %s from "%s"' %
1517 (revs
, opts
["source-url"]))
1519 report('merging in revision(s) %s from "%s"' %
1520 (revs
, opts
["source-url"]))
1522 # Do the merge(s). Note: the starting revision number to 'svn merge'
1523 # is NOT inclusive so we have to subtract one from start.
1524 # We try to keep the number of merge operations as low as possible,
1525 # because it is faster and reduces the number of conflicts.
1526 old_block_props
= get_block_props(branch_dir
)
1527 merge_metadata
= logs
[opts
["source-url"]].merge_metadata()
1528 block_metadata
= logs
[opts
["source-url"]].block_metadata()
1529 for start
,end
in minimal_merge_intervals(revs
, phantom_revs
):
1531 # Preset merge/blocked properties to the source value at
1532 # the start rev to avoid spurious property conflicts
1533 set_merge_props(branch_dir
, merge_metadata
.get(start
- 1))
1534 set_block_props(branch_dir
, block_metadata
.get(start
- 1))
1536 svn_command("merge --force -r %d:%d %s %s" % \
1537 (start
- 1, end
, opts
["source-url"], branch_dir
))
1538 # TODO: to support graph merging, add logic to merge the property
1539 # meta-data manually
1541 # Update the set of merged revisions.
1542 merged_revs
= merged_revs | revs | reflected_revs | phantom_revs | initialized_revs
1543 branch_props
[opts
["source-pathid"]] = str(merged_revs
)
1544 set_merge_props(branch_dir
, branch_props
)
1545 # Reset the blocked revs
1546 set_block_props(branch_dir
, old_block_props
)
1548 # Write out commit message if desired
1549 if opts
["commit-file"]:
1550 f
= open(opts
["commit-file"], "w")
1552 print >>f
, 'Recorded merge of revisions %s via %s from ' % \
1555 print >>f
, 'Merged revisions %s via %s from ' % \
1557 print >>f
, '%s' % opts
["source-url"]
1558 if opts
["commit-verbose"]:
1560 print >>f
, construct_merged_log_message(opts
["source-url"], revs
),
1563 report('wrote commit message to "%s"' % opts
["commit-file"])
1565 def action_block(branch_dir
, branch_props
):
1566 """Block revisions."""
1567 # Check branch directory is ready for being modified
1568 check_dir_clean(branch_dir
)
1570 source_revs
, phantom_revs
, reflected_revs
, initialized_revs
= \
1571 analyze_source_revs(branch_dir
, opts
["source-url"])
1572 revs_to_block
= source_revs
- opts
["merged-revs"]
1574 # Limit to revisions specified by -r (if any)
1575 if opts
["revision"]:
1576 revs_to_block
= RevisionSet(opts
["revision"]) & revs_to_block
1578 if not revs_to_block
:
1579 error('no available revisions to block')
1581 # Change blocked information
1582 blocked_revs
= get_blocked_revs(branch_dir
, opts
["source-pathid"])
1583 blocked_revs
= blocked_revs | revs_to_block
1584 set_blocked_revs(branch_dir
, opts
["source-pathid"], blocked_revs
)
1586 # Write out commit message if desired
1587 if opts
["commit-file"]:
1588 f
= open(opts
["commit-file"], "w")
1589 print >>f
, 'Blocked revisions %s via %s' % (revs_to_block
, NAME
)
1590 if opts
["commit-verbose"]:
1592 print >>f
, construct_merged_log_message(opts
["source-url"],
1596 report('wrote commit message to "%s"' % opts
["commit-file"])
1598 def action_unblock(branch_dir
, branch_props
):
1599 """Unblock revisions."""
1600 # Check branch directory is ready for being modified
1601 check_dir_clean(branch_dir
)
1603 blocked_revs
= get_blocked_revs(branch_dir
, opts
["source-pathid"])
1604 revs_to_unblock
= blocked_revs
1606 # Limit to revisions specified by -r (if any)
1607 if opts
["revision"]:
1608 revs_to_unblock
= revs_to_unblock
& RevisionSet(opts
["revision"])
1610 if not revs_to_unblock
:
1611 error('no available revisions to unblock')
1613 # Change blocked information
1614 blocked_revs
= blocked_revs
- revs_to_unblock
1615 set_blocked_revs(branch_dir
, opts
["source-pathid"], blocked_revs
)
1617 # Write out commit message if desired
1618 if opts
["commit-file"]:
1619 f
= open(opts
["commit-file"], "w")
1620 print >>f
, 'Unblocked revisions %s via %s' % (revs_to_unblock
, NAME
)
1621 if opts
["commit-verbose"]:
1623 print >>f
, construct_merged_log_message(opts
["source-url"],
1626 report('wrote commit message to "%s"' % opts
["commit-file"])
1628 def action_rollback(branch_dir
, branch_props
):
1629 """Rollback previously integrated revisions."""
1631 # Make sure the revision arguments are present
1632 if not opts
["revision"]:
1633 error("The '-r' option is mandatory for rollback")
1635 # Check branch directory is ready for being modified
1636 check_dir_clean(branch_dir
)
1638 # Extract the integration info for the branch_dir
1639 branch_props
= get_merge_props(branch_dir
)
1640 # Get the list of all revisions already merged into this source-pathid.
1641 merged_revs
= merge_props_to_revision_set(branch_props
,
1642 opts
["source-pathid"])
1644 # At which revision was the src created?
1645 oldest_src_rev
= get_created_rev(opts
["source-url"])
1646 src_pre_exist_range
= RevisionSet("1-%d" % oldest_src_rev
)
1648 # Limit to revisions specified by -r (if any)
1649 revs
= merged_revs
& RevisionSet(opts
["revision"])
1651 # make sure there's some revision to rollback
1653 report("Nothing to rollback in revision range r%s" % opts
["revision"])
1656 # If even one specified revision lies outside the lifetime of the
1657 # merge source, error out.
1658 if revs
& src_pre_exist_range
:
1659 err_str
= "Specified revision range falls out of the rollback range.\n"
1660 err_str
+= "%s was created at r%d" % (opts
["source-pathid"],
1664 record_only
= opts
["record-only"]
1667 report('recording rollback of revision(s) %s from "%s"' %
1668 (revs
, opts
["source-url"]))
1670 report('rollback of revision(s) %s from "%s"' %
1671 (revs
, opts
["source-url"]))
1673 # Do the reverse merge(s). Note: the starting revision number
1674 # to 'svn merge' is NOT inclusive so we have to subtract one from start.
1675 # We try to keep the number of merge operations as low as possible,
1676 # because it is faster and reduces the number of conflicts.
1677 rollback_intervals
= minimal_merge_intervals(revs
, [])
1678 # rollback in the reverse order of merge
1679 rollback_intervals
.reverse()
1680 for start
, end
in rollback_intervals
:
1683 svn_command("merge --force -r %d:%d %s %s" % \
1684 (end
, start
- 1, opts
["source-url"], branch_dir
))
1686 # Write out commit message if desired
1687 # calculate the phantom revs first
1688 if opts
["commit-file"]:
1689 f
= open(opts
["commit-file"], "w")
1691 print >>f
, 'Recorded rollback of revisions %s via %s from ' % \
1694 print >>f
, 'Rolled back revisions %s via %s from ' % \
1696 print >>f
, '%s' % opts
["source-url"]
1699 report('wrote commit message to "%s"' % opts
["commit-file"])
1701 # Update the set of merged revisions.
1702 merged_revs
= merged_revs
- revs
1703 branch_props
[opts
["source-pathid"]] = str(merged_revs
)
1704 set_merge_props(branch_dir
, branch_props
)
1706 def action_uninit(branch_dir
, branch_props
):
1707 """Uninit SOURCE URL."""
1708 # Check branch directory is ready for being modified
1709 check_dir_clean(branch_dir
)
1711 # If the source-pathid does not have an entry in the svnmerge-integrated
1712 # property, simply error out.
1713 if not branch_props
.has_key(opts
["source-pathid"]):
1714 error('Repository-relative path "%s" does not contain merge '
1715 'tracking information for "%s"' \
1716 % (opts
["source-pathid"], branch_dir
))
1718 del branch_props
[opts
["source-pathid"]]
1720 # Set merge property with the selected source deleted
1721 set_merge_props(branch_dir
, branch_props
)
1723 # Set blocked revisions for the selected source to None
1724 set_blocked_revs(branch_dir
, opts
["source-pathid"], None)
1726 # Write out commit message if desired
1727 if opts
["commit-file"]:
1728 f
= open(opts
["commit-file"], "w")
1729 print >>f
, 'Removed merge tracking for "%s" for ' % NAME
1730 print >>f
, '%s' % opts
["source-url"]
1732 report('wrote commit message to "%s"' % opts
["commit-file"])
1734 ###############################################################################
1735 # Command line parsing -- options and commands management
1736 ###############################################################################
1739 def __init__(self
, *args
, **kwargs
):
1740 self
.help = kwargs
["help"]
1745 if a
.startswith("--"): self
.lflags
.append(a
)
1746 elif a
.startswith("-"): self
.sflags
.append(a
)
1748 raise TypeError, "invalid flag name: %s" % a
1749 if kwargs
.has_key("dest"):
1750 self
.dest
= kwargs
["dest"]
1754 raise TypeError, "cannot deduce dest name without long options"
1755 self
.dest
= self
.lflags
[0][2:]
1757 raise TypeError, "invalid keyword arguments: %r" % kwargs
.keys()
1758 def repr_flags(self
):
1759 f
= self
.sflags
+ self
.lflags
1765 class Option(OptBase
):
1766 def __init__(self
, *args
, **kwargs
):
1767 self
.default
= kwargs
.setdefault("default", 0)
1768 del kwargs
["default"]
1769 self
.value
= kwargs
.setdefault("value", None)
1771 OptBase
.__init
__(self
, *args
, **kwargs
)
1772 def apply(self
, state
, value
):
1774 if self
.value
is not None:
1775 state
[self
.dest
] = self
.value
1777 state
[self
.dest
] += 1
1779 class OptionArg(OptBase
):
1780 def __init__(self
, *args
, **kwargs
):
1781 self
.default
= kwargs
["default"]
1782 del kwargs
["default"]
1783 self
.metavar
= kwargs
.setdefault("metavar", None)
1784 del kwargs
["metavar"]
1785 OptBase
.__init
__(self
, *args
, **kwargs
)
1787 if self
.metavar
is None:
1788 if self
.dest
is not None:
1789 self
.metavar
= self
.dest
.upper()
1791 self
.metavar
= "arg"
1793 self
.help += " (default: %s)" % self
.default
1794 def apply(self
, state
, value
):
1795 assert value
is not None
1796 state
[self
.dest
] = value
1797 def repr_flags(self
):
1798 r
= OptBase
.repr_flags(self
)
1799 return r
+ " " + self
.metavar
1803 def __init__(self
, *args
):
1804 self
.name
, self
.func
, self
.usage
, self
.help, self
.opts
= args
1805 def short_help(self
):
1806 return self
.help.split(".")[0]
1809 def __call__(self
, *args
, **kwargs
):
1810 return self
.func(*args
, **kwargs
)
1812 def __init__(self
, global_opts
, common_opts
, command_table
, version
=None):
1813 self
.progname
= NAME
1814 self
.version
= version
.replace("%prog", self
.progname
)
1815 self
.cwidth
= console_width() - 2
1816 self
.ctable
= command_table
.copy()
1817 self
.gopts
= global_opts
[:]
1818 self
.copts
= common_opts
[:]
1819 self
._add
_builtins
()
1820 for k
in self
.ctable
.keys():
1821 cmd
= self
.Cmd(k
, *self
.ctable
[k
])
1824 if isinstance(o
, types
.StringType
) or \
1825 isinstance(o
, types
.UnicodeType
):
1826 o
= self
._find
_common
(o
)
1829 self
.ctable
[k
] = cmd
1831 def _add_builtins(self
):
1833 Option("-h", "--help", help="show help for this command and exit"))
1834 if self
.version
is not None:
1836 Option("-V", "--version", help="show version info and exit"))
1837 self
.ctable
["help"] = (self
._cmd
_help
,
1839 "Display help for a specific command. If COMMAND is omitted, "
1840 "display brief command description.",
1843 def _cmd_help(self
, cmd
=None, *args
):
1845 self
.error("wrong number of arguments", "help")
1847 cmd
= self
._command
(cmd
)
1848 self
.print_command_help(cmd
)
1850 self
.print_command_list()
1852 def _paragraph(self
, text
, width
=78):
1853 chunks
= re
.split("\s+", text
.strip())
1858 while chunks
and len(L
) + len(chunks
[-1]) + 1 <= width
:
1859 L
+= " " + chunks
.pop()
1863 def _paragraphs(self
, text
, *args
, **kwargs
):
1864 pars
= text
.split("\n\n")
1865 lines
= self
._paragraph
(pars
[0], *args
, **kwargs
)
1868 lines
.extend(self
._paragraph
(p
, *args
, **kwargs
))
1871 def _print_wrapped(self
, text
, indent
=0):
1872 text
= self
._paragraphs
(text
, self
.cwidth
- indent
)
1875 print " " * indent
+ t
1877 def _find_common(self
, fl
):
1878 for o
in self
.copts
:
1879 if fl
in o
.lflags
+o
.sflags
:
1883 def _compute_flags(self
, opts
, check_conflicts
=True):
1889 if isinstance(o
, OptionArg
):
1890 sapp
, lapp
= ":", "="
1892 if check_conflicts
and back
.has_key(s
):
1893 raise RuntimeError, "option conflict: %s" % s
1897 if check_conflicts
and back
.has_key(l
):
1898 raise RuntimeError, "option conflict: %s" % l
1900 lfl
.append(l
[2:] + lapp
)
1901 return sfl
, lfl
, back
1903 def _extract_command(self
, args
):
1905 Try to extract the command name from the argument list. This is
1906 non-trivial because we want to allow command-specific options even
1907 before the command itself.
1909 opts
= self
.gopts
[:]
1910 for cmd
in self
.ctable
.values():
1911 opts
.extend(cmd
.opts
)
1912 sfl
, lfl
, _
= self
._compute
_flags
(opts
, check_conflicts
=False)
1914 lopts
,largs
= getopt
.getopt(args
, sfl
, lfl
)
1917 return self
._command
(largs
[0])
1919 def _fancy_getopt(self
, args
, opts
, state
=None):
1923 if not state
.has_key(o
.dest
):
1924 state
[o
.dest
] = o
.default
1926 sfl
, lfl
, back
= self
._compute
_flags
(opts
)
1928 lopts
,args
= getopt
.gnu_getopt(args
, sfl
, lfl
)
1929 except AttributeError:
1930 # Before Python 2.3, there was no gnu_getopt support.
1931 # So we can't parse intermixed positional arguments
1933 lopts
,args
= getopt
.getopt(args
, sfl
, lfl
)
1936 back
[o
].apply(state
, v
)
1939 def _command(self
, cmd
):
1940 if not self
.ctable
.has_key(cmd
):
1941 self
.error("unknown command: '%s'" % cmd
)
1942 return self
.ctable
[cmd
]
1944 def parse(self
, args
):
1946 self
.print_small_help()
1951 cmd
= self
._extract
_command
(args
)
1952 opts
= self
.gopts
[:]
1954 opts
.extend(cmd
.opts
)
1955 args
.remove(cmd
.name
)
1956 state
, args
= self
._fancy
_getopt
(args
, opts
)
1957 except getopt
.GetoptError
, e
:
1961 if self
.version
is not None and state
["version"]:
1962 self
.print_version()
1964 if state
["help"]: # special case for --help
1966 self
.print_command_help(cmd
)
1968 cmd
= self
.ctable
["help"]
1971 self
.error("command argument required")
1972 if str(cmd
) == "help":
1975 return cmd
, args
, state
1977 def error(self
, s
, cmd
=None):
1978 print >>sys
.stderr
, "%s: %s" % (self
.progname
, s
)
1980 self
.print_command_help(cmd
)
1982 self
.print_small_help()
1984 def print_small_help(self
):
1985 print "Type '%s help' for usage" % self
.progname
1986 def print_usage_line(self
):
1987 print "usage: %s <subcommand> [options...] [args...]\n" % self
.progname
1988 def print_command_list(self
):
1989 print "Available commands (use '%s help COMMAND' for more details):\n" \
1991 cmds
= self
.ctable
.keys()
1993 indent
= max(map(len, cmds
))
1995 h
= self
.ctable
[c
].short_help()
1996 print " %-*s " % (indent
, c
),
1997 self
._print
_wrapped
(h
, indent
+6)
1998 def print_command_help(self
, cmd
):
1999 cmd
= self
.ctable
[str(cmd
)]
2000 print 'usage: %s %s\n' % (self
.progname
, cmd
.usage
)
2001 self
._print
_wrapped
(cmd
.help)
2002 def print_opts(opts
, self
=self
):
2004 flags
= [o
.repr_flags() for o
in opts
]
2005 indent
= max(map(len, flags
))
2006 for f
,o
in zip(flags
, opts
):
2007 print " %-*s :" % (indent
, f
),
2008 self
._print
_wrapped
(o
.help, indent
+5)
2009 print '\nCommand options:'
2010 print_opts(cmd
.opts
)
2011 print '\nGlobal options:'
2012 print_opts(self
.gopts
)
2014 def print_version(self
):
2017 ###############################################################################
2018 # Options and Commands description
2019 ###############################################################################
2022 Option("-F", "--force",
2023 help="force operation even if the working copy is not clean, or "
2024 "there are pending updates"),
2025 Option("-n", "--dry-run",
2026 help="don't actually change anything, just pretend; "
2027 "implies --show-changes"),
2028 Option("-s", "--show-changes",
2029 help="show subversion commands that make changes"),
2030 Option("-v", "--verbose",
2031 help="verbose mode: output more information about progress"),
2032 OptionArg("-u", "--username",
2034 help="invoke subversion commands with the supplied username"),
2035 OptionArg("-p", "--password",
2037 help="invoke subversion commands with the supplied password"),
2038 OptionArg("-c", "--config-dir", metavar
="DIR",
2040 help="cause subversion commands to consult runtime config directory DIR"),
2044 Option("-b", "--bidirectional",
2047 help="remove reflected and initialized revisions from merge candidates. "
2048 "Not required but may be specified to speed things up slightly"),
2049 OptionArg("-f", "--commit-file", metavar
="FILE",
2050 default
="svnmerge-commit-message.txt",
2051 help="set the name of the file where the suggested log message "
2053 Option("-M", "--record-only",
2056 help="do not perform an actual merge of the changes, yet record "
2057 "that a merge happened"),
2058 OptionArg("-r", "--revision",
2061 help="specify a revision list, consisting of revision numbers "
2062 'and ranges separated by commas, e.g., "534,537-539,540"'),
2063 OptionArg("-S", "--source", "--head",
2065 help="specify a merge source for this branch. It can be either "
2066 "a working directory path, a full URL, or an unambiguous "
2067 "substring of one of the locations for which merge tracking was "
2068 "already initialized. Needed only to disambiguate in case of "
2069 "multiple merge sources"),
2073 "init": (action_init
,
2074 "init [OPTION...] [SOURCE]",
2075 """Initialize merge tracking from SOURCE on the current working
2078 If SOURCE is specified, all the revisions in SOURCE are marked as already
2079 merged; if this is not correct, you can use --revision to specify the
2080 exact list of already-merged revisions.
2082 If SOURCE is omitted, then it is computed from the "svn cp" history of the
2083 current working directory (searching back for the branch point); in this
2084 case, %s assumes that no revision has been integrated yet since
2085 the branch point (unless you teach it with --revision).""" % NAME
,
2087 "-f", "-r", # import common opts
2088 OptionArg("-L", "--location-type",
2089 dest
="location-type",
2091 help="Use this type of location identifier in the new " +
2092 "Subversion properties; 'uuid', 'url', or 'path' " +
2096 "avail": (action_avail
,
2097 "avail [OPTION...] [PATH]",
2098 """Show unmerged revisions available for PATH as a revision list.
2099 If --revision is given, the revisions shown will be limited to those
2100 also specified in the option.
2102 When svnmerge is used to bidirectionally merge changes between a
2103 branch and its source, it is necessary to not merge the same changes
2104 forth and back: e.g., if you committed a merge of a certain
2105 revision of the branch into the source, you do not want that commit
2106 to appear as available to merged into the branch (as the code
2107 originated in the branch itself!). svnmerge will automatically
2108 exclude these so-called "reflected" revisions.""",
2110 Option("-A", "--all",
2111 dest
="avail-showwhat",
2112 value
=["blocked", "avail"],
2114 help="show both available and blocked revisions (aka ignore "
2115 "blocked revisions)"),
2117 Option("-B", "--blocked",
2118 dest
="avail-showwhat",
2120 help="show the blocked revision list (see '%s block')" % NAME
),
2121 Option("-d", "--diff",
2122 dest
="avail-display",
2124 default
="revisions",
2125 help="show corresponding diff instead of revision list"),
2126 Option("--summarize",
2127 dest
="avail-display",
2129 help="show summarized diff instead of revision list"),
2130 Option("-l", "--log",
2131 dest
="avail-display",
2133 help="show corresponding log history instead of revision list"),
2138 "integrated": (action_integrated
,
2139 "integrated [OPTION...] [PATH]",
2140 """Show merged revisions available for PATH as a revision list.
2141 If --revision is given, the revisions shown will be limited to
2142 those also specified in the option.""",
2144 Option("-d", "--diff",
2145 dest
="integrated-display",
2147 default
="revisions",
2148 help="show corresponding diff instead of revision list"),
2149 Option("-l", "--log",
2150 dest
="integrated-display",
2152 help="show corresponding log history instead of revision list"),
2157 "rollback": (action_rollback
,
2158 "rollback [OPTION...] [PATH]",
2159 """Rollback previously merged in revisions from PATH. The
2160 --revision option is mandatory, and specifies which revisions
2161 will be rolled back. Only the previously integrated merges
2162 will be rolled back.
2164 When manually rolling back changes, --record-only can be used to
2165 instruct %s that a manual rollback of a certain revision
2166 already happened, so that it can record it and offer that
2167 revision for merge henceforth.""" % (NAME
),
2169 "-f", "-r", "-S", "-M", # import common opts
2172 "merge": (action_merge
,
2173 "merge [OPTION...] [PATH]",
2174 """Merge in revisions into PATH from its source. If --revision is omitted,
2175 all the available revisions will be merged. In any case, already merged-in
2176 revisions will NOT be merged again.
2178 When svnmerge is used to bidirectionally merge changes between a
2179 branch and its source, it is necessary to not merge the same changes
2180 forth and back: e.g., if you committed a merge of a certain
2181 revision of the branch into the source, you do not want that commit
2182 to appear as available to merged into the branch (as the code
2183 originated in the branch itself!). svnmerge will automatically
2184 exclude these so-called "reflected" revisions.
2186 When manually merging changes across branches, --record-only can
2187 be used to instruct %s that a manual merge of a certain revision
2188 already happened, so that it can record it and not offer that
2189 revision for merge anymore. Conversely, when there are revisions
2190 which should not be merged, use '%s block'.""" % (NAME
, NAME
),
2192 "-b", "-f", "-r", "-S", "-M", # import common opts
2195 "block": (action_block
,
2196 "block [OPTION...] [PATH]",
2197 """Block revisions within PATH so that they disappear from the available
2198 list. This is useful to hide revisions which will not be integrated.
2199 If --revision is omitted, it defaults to all the available revisions.
2201 Do not use this option to hide revisions that were manually merged
2202 into the branch. Instead, use '%s merge --record-only', which
2203 records that a merge happened (as opposed to a merge which should
2204 not happen).""" % NAME
,
2206 "-f", "-r", "-S", # import common opts
2209 "unblock": (action_unblock
,
2210 "unblock [OPTION...] [PATH]",
2211 """Revert the effect of '%s block'. If --revision is omitted, all the
2212 blocked revisions are unblocked""" % NAME
,
2214 "-f", "-r", "-S", # import common opts
2217 "uninit": (action_uninit
,
2218 "uninit [OPTION...] [PATH]",
2219 """Remove merge tracking information from PATH. It cleans any kind of merge
2220 tracking information (including the list of blocked revisions). If there
2221 are multiple sources, use --source to indicate which source you want to
2224 "-f", "-S", # import common opts
2232 # Initialize default options
2233 opts
= default_opts
.copy()
2236 optsparser
= CommandOpts(global_opts
, common_opts
, command_table
,
2237 version
="%%prog r%s\n modified: %s\n\n"
2238 "Copyright (C) 2004,2005 Awarix Inc.\n"
2239 "Copyright (C) 2005, Giovanni Bajo"
2240 % (__revision__
, __date__
))
2242 cmd
, args
, state
= optsparser
.parse(args
)
2245 source
= opts
.get("source", None)
2248 if str(cmd
) == "init":
2252 optsparser
.error("wrong number of parameters", cmd
)
2253 elif str(cmd
) in command_table
.keys():
2255 branch_dir
= args
[0]
2257 optsparser
.error("wrong number of parameters", cmd
)
2259 assert False, "command not handled: %s" % cmd
2261 # Validate branch_dir
2262 if not is_wc(branch_dir
):
2263 if str(cmd
) == "avail":
2265 # it should be noted here that svn info does not error exit
2266 # if an invalid target is specified to it (as is
2267 # intuitive). so the try, except code is not absolutely
2268 # necessary. but, I retain it to indicate the intuitive
2271 info
= get_svninfo(branch_dir
)
2274 # test that we definitely targeted a subversion directory,
2275 # mirroring the purpose of the earlier is_wc() call
2276 if info
is None or not info
.has_key("Node Kind") or info
["Node Kind"] != "directory":
2277 error('"%s" is neither a valid URL, nor a working directory' % branch_dir
)
2279 error('"%s" is not a subversion working directory' % branch_dir
)
2281 # give out some hints as to potential pathids
2282 PathIdentifier
.hint(branch_dir
)
2283 if source
: PathIdentifier
.hint(source
)
2285 # Extract the integration info for the branch_dir
2286 branch_props
= get_merge_props(branch_dir
)
2288 # Calculate source_url and source_path
2289 report("calculate source path for the branch")
2291 if str(cmd
) == "init":
2292 cf_source
, cf_rev
, copy_committed_in_rev
= get_copyfrom(branch_dir
)
2294 error('no copyfrom info available. '
2295 'Explicit source argument (-S/--source) required.')
2296 opts
["source-url"] = get_repo_root(branch_dir
) + cf_source
2297 opts
["source-pathid"] = PathIdentifier
.from_target(opts
["source-url"])
2299 if not opts
["revision"]:
2300 opts
["revision"] = "1-" + cf_rev
2302 opts
["source-pathid"] = get_default_source(branch_dir
, branch_props
)
2303 opts
["source-url"] = opts
["source-pathid"].get_url()
2305 assert is_pathid(opts
["source-pathid"])
2306 assert is_url(opts
["source-url"])
2308 # The source was given as a command line argument and is stored in
2309 # SOURCE. Ensure that the specified source does not end in a /,
2310 # otherwise it's easy to have the same source path listed more
2311 # than once in the integrated version properties, with and without
2313 source
= rstrip(source
, "/")
2314 if not is_wc(source
) and not is_url(source
):
2315 # Check if it is a substring of a pathid recorded
2316 # within the branch properties.
2318 for pathid
in branch_props
.keys():
2319 if pathid
.match_substring(source
):
2320 found
.append(pathid
)
2322 # (assumes pathid is a repository-relative-path)
2323 source_pathid
= found
[0]
2324 source
= source_pathid
.get_url()
2326 error('"%s" is neither a valid URL, nor an unambiguous '
2327 'substring of a repository path, nor a working directory'
2330 source_pathid
= PathIdentifier
.from_target(source
)
2332 source_pathid
= PathIdentifier
.from_target(source
)
2333 if str(cmd
) == "init" and \
2334 source_pathid
== PathIdentifier
.from_target("."):
2335 error("cannot init integration source path '%s'\n"
2336 "Its repository-relative path must differ from the "
2337 "repository-relative path of the current directory."
2339 opts
["source-pathid"] = source_pathid
2340 opts
["source-url"] = target_to_url(source
)
2342 # Sanity check source_url
2343 assert is_url(opts
["source-url"])
2344 # SVN does not support non-normalized URL (and we should not
2345 # have created them)
2346 assert opts
["source-url"].find("/..") < 0
2348 report('source is "%s"' % opts
["source-url"])
2350 # Get previously merged revisions (except when command is init)
2351 if str(cmd
) != "init":
2352 opts
["merged-revs"] = merge_props_to_revision_set(branch_props
,
2353 opts
["source-pathid"])
2355 # Perform the action
2356 cmd(branch_dir
, branch_props
)
2359 if __name__
== "__main__":
2362 except LaunchError
, (ret
, cmd
, out
):
2363 err_msg
= "command execution failed (exit code: %d)\n" % ret
2364 err_msg
+= cmd
+ "\n"
2365 err_msg
+= "".join(out
)
2367 except KeyboardInterrupt:
2368 # Avoid traceback on CTRL+C
2369 print "aborted by user"