📄 svnmerge.py
字号:
#!/usr/bin/env python# -*- coding: utf-8 -*-# Copyright (c) 2005, Giovanni Bajo# Copyright (c) 2004-2005, Awarix, Inc.# All rights reserved.## This program is free software; you can redistribute it and/or# modify it under the terms of the GNU General Public License# as published by the Free Software Foundation; either version 2# of the License, or (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with this program; if not, write to the Free Software# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA## Author: Archie Cobbs <archie at awarix dot com># Rewritten in Python by: Giovanni Bajo <rasky at develer dot com>## Acknowledgments:# John Belmonte <john at neggie dot net> - metadata and usability# improvements# Blair Zajac <blair at orcaware dot com> - random improvements# Raman Gupta <rocketraman at fastmail dot fm> - bidirectional merging# support## $HeadURL: https://svn.collab.net/repos/svn/branches/1.4.x/contrib/client-side/svnmerge.py $# $LastChangedDate: 2006-10-17 01:52:10 -0500 (Tue, 17 Oct 2006) $# $LastChangedBy: glasser $# $LastChangedRevision: 21994 $## Requisites:# svnmerge.py has been tested with all SVN major versions since 1.1 (both# client and server). It is unknown if it works with previous versions.## Differences from svnmerge.sh:# - More portable: tested as working in FreeBSD and OS/2.# - Add double-verbose mode, which shows every svn command executed (-v -v).# - "svnmerge avail" now only shows commits in head, not also commits in other# parts of the repository.# - Add "svnmerge block" to flag some revisions as blocked, so that# they will not show up anymore in the available list. Added also# the complementary "svnmerge unblock".# - "svnmerge avail" has grown two new options:# -B to display a list of the blocked revisions# -A to display both the blocked and the available revisions.# - Improved generated commit message to make it machine parsable even when# merging commits which are themselves merges.# - Add --force option to skip working copy check# - Add --record-only option to "svnmerge merge" to avoid performing# an actual merge, yet record that a merge happened.## TODO:# - Add "svnmerge avail -R": show logs in reverse orderimport sys, os, getopt, re, types, popen2, tempfilefrom bisect import bisectNAME = "svnmerge"if not hasattr(sys, "version_info") or sys.version_info < (2, 0): error("requires Python 2.0 or newer")# Set up the separator used to separate individual log messages from# each revision merged into the target location. Also, create a# regular expression that will find this same separator in already# committed log messages, so that the separator used for this run of# svnmerge.py will have one more LOG_SEPARATOR appended to the longest# separator found in all the commits.LOG_SEPARATOR = 8 * '.'LOG_SEPARATOR_RE = re.compile('^((%s)+)' % re.escape(LOG_SEPARATOR), re.MULTILINE)# Each line of the embedded log messages will be prefixed by LOG_LINE_PREFIX.LOG_LINE_PREFIX = 2 * ' '# We expect non-localized output from SVNos.environ["LC_MESSAGES"] = "C"################################################################################ Support for older Python versions################################################################################ True/False constants are Python 2.2+try: True, Falseexcept NameError: True, False = 1, 0def lstrip(s, ch): """Replacement for str.lstrip (support for arbitrary chars to strip was added in Python 2.2.2).""" i = 0 try: while s[i] == ch: i = i+1 return s[i:] except IndexError: return ""def rstrip(s, ch): """Replacement for str.rstrip (support for arbitrary chars to strip was added in Python 2.2.2).""" try: if s[-1] != ch: return s i = -2 while s[i] == ch: i = i-1 return s[:i+1] except IndexError: return ""def strip(s, ch): """Replacement for str.strip (support for arbitrary chars to strip was added in Python 2.2.2).""" return lstrip(rstrip(s, ch), ch)def rsplit(s, sep, maxsplits=0): """Like str.rsplit, which is Python 2.4+ only.""" L = s.split(sep) if not 0 < maxsplits <= len(L): return L return [sep.join(L[0:-maxsplits])] + L[-maxsplits:]###############################################################################def kwextract(s): """Extract info from a svn keyword string.""" try: return strip(s, "$").strip().split(": ")[1] except IndexError: return "<unknown>"__revision__ = kwextract('$Rev: 21994 $')__date__ = kwextract('$Date: 2006-10-17 01:52:10 -0500 (Tue, 17 Oct 2006) $')# Additional options, not (yet?) mapped to command line flagsdefault_opts = { "svn": "svn", "prop": NAME + "-integrated", "block-prop": NAME + "-blocked", "commit-verbose": True,}logs = {}def console_width(): """Get the width of the console screen (if any).""" try: return int(os.environ["COLUMNS"]) except (KeyError, ValueError): pass try: # Call the Windows API (requires ctypes library) from ctypes import windll, create_string_buffer h = windll.kernel32.GetStdHandle(-11) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: import struct (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) return right - left + 1 except ImportError: pass # Parse the output of stty -a out = os.popen("stty -a").read() m = re.search(r"columns (\d+);", out) if m: return int(m.group(1)) # sensible default return 80def error(s): """Subroutine to output an error and bail.""" print >> sys.stderr, "%s: %s" % (NAME, s) sys.exit(1)def report(s): """Subroutine to output progress message, unless in quiet mode.""" if opts["verbose"]: print "%s: %s" % (NAME, s)def prefix_lines(prefix, lines): """Given a string representing one or more lines of text, insert the specified prefix at the beginning of each line, and return the result. The input must be terminated by a newline.""" assert lines[-1] == "\n" return prefix + lines[:-1].replace("\n", "\n"+prefix) + "\n"class LaunchError(Exception): """Signal a failure in execution of an external command. Parameters are the exit code of the process, the original command line, and the output of the command."""def launch(cmd, split_lines=True): """Launch a sub-process. Return its output (both stdout and stderr), optionally split by lines (if split_lines is True). Raise a LaunchError exception if the exit code of the process is non-zero (failure).""" if os.name not in ['nt', 'os2']: p = popen2.Popen4(cmd) p.tochild.close() if split_lines: out = p.fromchild.readlines() else: out = p.fromchild.read() ret = p.wait() if ret == 0: ret = None else: ret >>= 8 else: i,k = os.popen4(cmd) i.close() if split_lines: out = k.readlines() else: out = k.read() ret = k.close() if ret is None: return out raise LaunchError(ret, cmd, out)def launchsvn(s, show=False, pretend=False, **kwargs): """Launch SVN and grab its output.""" username = opts.get("username", None) password = opts.get("password", None) if username: username = " --username=" + username else: username = "" if password: password = " --password=" + password else: password = "" cmd = opts["svn"] + username + password + " " + s if show or opts["verbose"] >= 2: print cmd if pretend: return None return launch(cmd, **kwargs)def svn_command(s): """Do (or pretend to do) an SVN command.""" out = launchsvn(s, show=opts["show-changes"] or opts["dry-run"], pretend=opts["dry-run"], split_lines=False) if not opts["dry-run"]: print outdef check_dir_clean(dir): """Check the current status of dir for local mods.""" if opts["force"]: report('skipping status check because of --force') return report('checking status of "%s"' % dir) # Checking with -q does not show unversioned files or external # directories. Though it displays a debug message for external # directories, after a blank line. So, practically, the first line # matters: if it's non-empty there is a modification. out = launchsvn("status -q %s" % dir) if out and out[0].strip(): error('"%s" has local modifications; it must be clean' % dir)class RevisionLog: """ A log of the revisions which affected a given URL between two revisions. """ def __init__(self, url, begin, end, find_propchanges=False): """ Create a new RevisionLog object, which stores, in self.revs, a list of the revisions which affected the specified URL between begin and end. If find_propchanges is True, self.propchange_revs will contain a list of the revisions which changed properties directly on the specified URL. URL must be the URL for a directory in the repository. """ # Save the specified URL self.url = url # Look for revisions revision_re = re.compile(r"^r(\d+)") # Look for changes which contain merge tracking information repos_path = target_to_repos_relative_path(url) srcdir_change_re = re.compile(r"\s*M\s+%s\s+$" % re.escape(repos_path)) # Setup the log options (--quiet, so we don't show log messages) log_opts = '--quiet -r%s:%s "%s"' % (begin, end, url) if find_propchanges: # The --verbose flag lets us grab merge tracking information # by looking at propchanges log_opts = "--verbose " + log_opts # Read the log to look for revision numbers and merge-tracking info self.revs = [] self.propchange_revs = [] for line in launchsvn("log %s" % log_opts): m = revision_re.match(line) if m: rev = int(m.groups()[0]) self.revs.append(rev) elif srcdir_change_re.match(line): self.propchange_revs.append(rev) # Save the range of the log self.begin = int(begin) if end == "HEAD": # If end is not provided, we do not know which is the latest # revision in the repository. So we set 'end' to the latest # known revision. self.end = log.revs[-1] else: self.end = int(end) self._merges = None def merge_metadata(self): """ Return a VersionedProperty object, with a cached view of the merge metadata in the range of this log. """ # Load merge metadata if necessary if not self._merges: self._merges = VersionedProperty(self.url, opts["prop"]) self._merges.load(self) return self._mergesclass VersionedProperty: """ A read-only, cached view of a versioned property. self.revs contains a list of the revisions in which the property changes. self.values stores the new values at each corresponding revision. If the value of the property is unknown, it is set to None. Initially, we set self.revs to [0] and self.values to [None]. This indicates that, as of revision zero, we know nothing about the value of the property. Later, if you run self.load(log), we cache the value of this property over the entire range of the log by noting each revision in which the property was changed. At the end of the range of the log, we invalidate our cache by adding the value "None" to our cache for any revisions which fall out
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -