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