[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Re: svn commit: r39884 - in trunk: . contrib/server-side/svncutter

From: Greg Stein <gstein_at_gmail.com>
Date: Mon, 12 Oct 2009 02:01:26 -0400

Great to see this!

Thank you, Eric.

On Thu, Oct 8, 2009 at 17:44, Eric S. Raymond <esr_at_thyrsus.com> wrote:
> Author: esr
> Date: Thu Oct  8 14:44:00 2009
> New Revision: 39884
>
> Log:
> Initial commit of svncutter.
>
> Added:
>   trunk/contrib/server-side/svncutter/
>   trunk/contrib/server-side/svncutter/README
>   trunk/contrib/server-side/svncutter/svncutter   (contents, props changed)
> Modified:
>   trunk/COMMITTERS
>
> Modified: trunk/COMMITTERS
> URL: http://svn.collab.net/viewvc/svn/trunk/COMMITTERS?pathrev=39884&r1=39883&r2=39884
> ==============================================================================
> --- trunk/COMMITTERS    Thu Oct  8 12:47:36 2009        (r39883)
> +++ trunk/COMMITTERS    Thu Oct  8 14:44:00 2009        (r39884)
> @@ -138,6 +138,7 @@ Commit access for specific areas:
>          nmiyo   MIYOKAWA, Nobuyoshi <n-miyo_at_tempus.org>     (www: ja)
>        rocksun   Rock Sun <daijun_at_gmail.com>                 (www: zh)
>        kmradke   Kevin Radke <kmradke_at_gmail.com>             (add-needs-lock.py)
> +           esr   Eric S. Raymond <esr_at_thyrsus.com>           (svncutter)
>
>   Translation of message files:
>
>
> Added: trunk/contrib/server-side/svncutter/README
> URL: http://svn.collab.net/viewvc/svn/trunk/contrib/server-side/svncutter/README?pathrev=39884
> ==============================================================================
> --- /dev/null   00:00:00 1970   (empty, because file is newly added)
> +++ trunk/contrib/server-side/svncutter/README  Thu Oct  8 14:44:00 2009        (r39884)
> @@ -0,0 +1,5 @@
> +svncutter is a tool for performing various surgical operations on
> +Subversion dump files.
> +
> +This directory exists to hold svncutter and some unit tests that don't exist
> +yet.
>
> Added: trunk/contrib/server-side/svncutter/svncutter
> URL: http://svn.collab.net/viewvc/svn/trunk/contrib/server-side/svncutter/svncutter?pathrev=39884
> ==============================================================================
> --- /dev/null   00:00:00 1970   (empty, because file is newly added)
> +++ trunk/contrib/server-side/svncutter/svncutter       Thu Oct  8 14:44:00 2009        (r39884)
> @@ -0,0 +1,692 @@
> +#!/usr/bin/env python
> +#
> +# Hacked together by ESR, October 2009. New BSD license applies.
> +# The Subversion project is explicitly granted permission to redistribute
> +# under the prevailing license of their project.
> +
> +"""
> +svncutter - clique-squash, range-selection, and property mutations on SVN dump files
> +general usage: svncutter [-q] [-r SELECTION] SUBCOMMAND
> +
> +In all commands, the -r (or --range) option limits the selection of revisions
> +over which an operation will be performed. A selection consists of
> +one or more comma-separated ranges. A range may consist of an integer
> +revision number or the special name HEAD for the head revision. Or it
> +may be a colon-separated pair of integers, ir an integer followed by a
> +colon followed by HEAD.
> +
> +Normally, each subcommand produces a progress spinner on standard
> +error; each turn means another revision has been filtered. The -q (or
> +--quiet) option suppresses this.
> +
> +Type 'svncutter help <subcommand>' for help on a specific subcommand.
> +
> +Available subcommands:
> +   squash
> +   select
> +   propdel
> +   propset
> +   proprename
> +   log
> +   setlog
> +"""
> +
> +oneliners = {
> +    "squash":     "Squashing revisions",
> +    "select":     "Selecting revisions",
> +    "propdel":    "Deleting revision properties",
> +    "propset":    "Setting revision properties",
> +    "proprename": "Renaming revision properties",
> +    "log":        "Extracting log entries",
> +    "setlog":     "Mutating log entries",
> +    }
> +
> +helpdict = {
> +    "squash": """\
> +squash: usage: svncutter [-q] [-r SELECTION] [-m mapfile] [-f] [-c] squash
> +
> +The 'squash' subcommand merges adjacent commits that have the same
> +author and log text and were made within 5 minutes of each other.
> +This can be helpful in cleaning up after migrations from file-oriented
> +revision control systems, or if a developer has been using a pre-2006
> +version of Emacs VC.
> +
> +With the -m (or --mapfile) option, squash emits a map to tne named
> +file showing how old revision numbers map into new ones.
> +
> +With the -e (or --excise) option, the specified set of revisions in
> +unconditionally removed.  The tool will exit with an error if an
> +excised remove is part of a clique eligible for squashing.  Note that
> +svncutter does not perform any checks on whether the repository
> +history is afterwards valid; if you delete a node using this option,
> +you won't find out you have a problem intil you attempt to load the
> +resulting dumpfile.
> +
> +svncutter attempts to fix up references to Subversion revisions in log
> +entries so they will still be correct after squashing.  It considers
> +anything that looks like the regular expression \\br[0-9]+\\b to be
> +a comment reference (this is the same format that Subversion uses
> +in log headers).
> +
> +Every revision in the file after the first omiited onf gets the property
> +'svncutter:original' set to the revision number it had before the
> +squash operation.
> +
> +The option --f (or --flagrefs) causes svncutter to wrap its revision-reference
> +substitutions in curly braces ({}).  By doing this, then grepping for 'r{'
> +in the output of 'svncutter log', you can check for false conversions.
> +
> +The -c (or --compressmap) option changes the mapfile format to one
> +that is easier for human browsing, though less well suited for
> +interpretation by other programs.
> +""",
> +    "select": """\
> +select: usage: svncutter [-q] [-r SELECTION] select
> +
> +The 'select' subcommand selects a range and permits only revisions in
> +that range to pass to standard output.  A range beginning with 0
> +includes the dumpfile header.
> +""",
> +    "propdel": """\
> +propdel: usage: svncutter ---revprop PROPNAME [-r SELECTION] propdel
> +
> +Delete the unversioned revision property PROPNAME. May
> +be restricted by a revision selection. You may specify multiple
> +prperties to be deleted.
> +""",
> +    "propset": """\
> +propset: usage: svncutter ---revprop PROPNAME=PROPVAL [-r SELECTION] propset
> +
> +Set the unversioned revision property PROPNAME to PROPVAL. May
> +be restricted by a revision selection. You may specify multiple
> +prperties to be deleted.
> +""",
> +    "proprename": """\
> +proprename: usage: svncutter ---revprop OLDNAME->NEWNAME [-r SELECTION] proprename
> +
> +Rename the unversioned revision property OLDNAME to NEWNAME. May
> +be restricted by a revision selection. You may specify multiple
> +prperties to be renamed.
> +""",
> +    "log": """\
> +log: usage: svncutter [-r SELECTION] log
> +
> +Generate a log report, same format as the output of svn log on a
> +repository, to standard output.
> +""",
> +    "setlog": """\
> +setlog: usage: svncutter [-r SELECTION] --logentries=LOGFILE setlog
> +
> +Replace the log entries in the input dumpfile with the corresponding entries
> +in the LOGFILE, which should be in the format of an svn log output.
> +Replacements may be restricted to a specified range.
> +""",
> +    }
> +
> +import os, sys, calendar, time, getopt, re
> +
> +class Baton:
> +    "Ship progress indications to stderr."
> +    def __init__(self, prompt, endmsg=None):
> +        self.stream = sys.stderr
> +        self.stream.write(prompt + "...")
> +        if os.isatty(self.stream.fileno()):
> +            self.stream.write(" \010")
> +        self.stream.flush()
> +        self.count = 0
> +        self.endmsg = endmsg
> +        self.time = time.time()
> +        return
> +
> +    def twirl(self, ch=None):
> +        if self.stream is None:
> +            return
> +        if os.isatty(self.stream.fileno()):
> +            if ch:
> +                self.stream.write(ch)
> +            else:
> +                self.stream.write("-/|\\"[self.count % 4])
> +                self.stream.write("\010")
> +            self.stream.flush()
> +        self.count = self.count + 1
> +        return
> +
> +    def end(self, msg=None):
> +        if msg == None:
> +            msg = self.endmsg
> +        if self.stream:
> +            self.stream.write(("...(%2.2f sec) %s." % (time.time() - self.time, msg)) + os.linesep)
> +        return
> +
> +class LineBufferedSource:
> +    "Generic class for line-buffered input with pushback."
> +    def __init__(self, infile):
> +        self.linebuffer = None
> +        self.file = infile
> +        self.linenumber = 0
> +    def readline(self):
> +        "Line-buffered readline."
> +        if self.linebuffer:
> +            line = self.linebuffer
> +            self.linebuffer = None
> +        else:
> +            line = self.file.readline()
> +            self.linenumber += 1
> +        return line
> +    def require(self, prefix):
> +        "Read a line, require it to have a specified prefix."
> +        line = self.readline()
> +        if not line:
> +            sys.stderr.write("svncutter: unexpected end of input." + os.linesep)
> +            sys.exit(1)
> +        assert line.startswith(prefix)
> +        return line
> +    def read(self, len):
> +        "Straight read from underlying file, no buffering."
> +        assert(self.linebuffer is None)
> +        text = self.file.read(len)
> +        self.linenumber += text.count(os.linesep[0])
> +        return text
> +    def peek(self):
> +        "Peek at the next line in the source."
> +        assert(self.linebuffer is None)
> +        self.linebuffer = self.file.readline()
> +        return self.linebuffer
> +    def flush(self):
> +        "Get the contents of the line buffer, clearing it."
> +        assert(self.linebuffer is not None)
> +        line = self.linebuffer
> +        self.linebuffer = None
> +        return line
> +    def push(self, line):
> +        "Push a line back to the line buffer."
> +        assert(self.linebuffer is None)
> +        self.linebuffer = line
> +    def has_line_buffered(self):
> +        return self.linebuffer is not None
> +
> +class DumpfileSource(LineBufferedSource):
> +    "This class knows about dumpfile format."
> +    def __init__(self, infile, baton=None):
> +        LineBufferedSource.__init__(self, infile)
> +        self.baton = baton
> +    def read_revision_header(self, property_hook=None):
> +        "Read a revision header, parsing its proprties."
> +        properties = {}
> +        propkeys = []
> +        stash = self.require("Revision-number:")
> +        revision = int(stash.split()[1])
> +        stash += self.require("Prop-content-length:")
> +        stash += self.require("Content-length:")
> +        stash += self.require(os.linesep)
> +        while not self.peek().startswith("PROPS-END"):
> +            self.require("K")
> +            keyhd = self.readline()
> +            key = keyhd.strip()
> +            valhd = self.require("V")
> +            vlen = int(valhd.split()[1])
> +            value = self.read(vlen)
> +            self.require(os.linesep)
> +            properties[key] = value
> +            propkeys.append(key)
> +        if property_hook:
> +            (propkeys, properties) = property_hook(propkeys, properties, revision)
> +        for key in propkeys:
> +            if key in properties:
> +                stash += "K %d%s" % (len(key), os.linesep)
> +                stash += "%s%s" % (key, os.linesep)
> +                stash += "V %d%s" % (len(properties[key]), os.linesep)
> +                stash += "%s%s" % (properties[key], os.linesep)
> +        stash += self.flush()
> +        if self.baton:
> +            self.baton.twirl()
> +        return (revision, stash, properties)
> +    def read_until_next(self, prefix, revmap=None):
> +        "Accumulate lines until the next matches a specified prefix."
> +        stash = ""
> +        while True:
> +            line = self.readline()
> +            if not line:
> +                return stash
> +            elif line.startswith(prefix):
> +                self.push(line)
> +                return stash
> +            else:
> +                # Hack the revision levels in copy-from headers.
> +                # We're actually modifying the dumpfile contents
> +                # (rather than selectively omitting parts of it).
> +                # Note: this will break on a dumpfile that has dumpfiles
> +                # in its nodes!
> +                if revmap and line.startswith("Node-copyfrom-rev:"):
> +                    oldrev = line.split()[1]
> +                    line = line.replace(oldrev, `revmap[int(oldrev)]`)
> +                stash += line
> +    def apply_property_hook(self, selection, hook):
> +        "Apply a property transformation on a specified range."
> +        def innerhook(keyprops, propdict, revision):
> +            if revision in selection:
> +                return hook(keyprops, propdict, revision)
> +            else:
> +                return (keyprops, propdict)
> +        while True:
> +            sys.stdout.write(self.read_until_next("Revision-number:"))
> +            if not self.has_line_buffered():
> +                return
> +            else:
> +                (revision,stash,properties) = self.read_revision_header(innerhook)
> +                sys.stdout.write(stash)
> +
> +    def __del__(self):
> +        if self.baton:
> +            self.baton.end()
> +
> +class SubversionRange:
> +    def __init__(self, txt):
> +        self.txt = txt
> +        self.intervals = []
> +        for (i, item) in enumerate(txt.split(",")):
> +            if ':' in item:
> +                (lower, upper) = item.split(':')
> +            else:
> +                lower = upper = item
> +            if lower.isdigit():
> +                lower = int(lower)
> +            if upper.isdigit():
> +                upper = int(upper)
> +            self.intervals.append((lower, upper))
> +    def __contains__(self, rev):
> +        for (lower, upper) in self.intervals:
> +            if lower == "HEAD":
> +                sys.stderr.write("svncutter: can't accept HEAD as lower bound of a range.\n")
> +                sys.exit(1)
> +            elif upper == "HEAD" or rev in range(lower, upper+1):
> +                return True
> +        return False
> +    def upperbound(self):
> +        "What is the uppermost revision in the spec?"
> +        if self.intervals[-1][1] == "HEAD":
> +            return sys.maxint
> +        else:
> +            return self.intervals[-1][1]
> +    def __repr__(self):
> +        return self.txt
> +
> +class Logfile:
> +    "Represent the state of a lofile"
> +    def __init__(self, readable, restriction=None):
> +        self.comments = {}
> +        self.source = LineBufferedSource(readable)
> +        state = 'awaiting_header'
> +        author = date = None
> +        logentry = ""
> +        lineno = 0
> +        while True:
> +            lineno += 1
> +            line = readable.readline()
> +            if state == 'in_logentry':
> +                if not line or line.startswith("-----------"):
> +                    if rev:
> +                        logentry = logentry.strip()
> +                        if restriction is None or rev in restriction:
> +                            self.comments[rev] = (author, date, logentry)
> +                        rev = None
> +                        logentry = ""
> +                    if line:
> +                        state = 'awaiting_header'
> +                    else:
> +                        break
> +                else:
> +                    logentry += line
> +            elif state == 'awaiting_header':
> +                if not line:
> +                    break
> +                elif line.startswith("-----------"):
> +                    continue
> +                else:
> +                    m = re.match("r[0-9]+", line)
> +                    if not m:
> +                        sys.stderr.write('"%s", line %d: svncutter did not see a comment header where one was expected\n' % (readable.name, lineno))
> +                        sys.exit(1)
> +                    else:
> +                        fields = line.split("|")
> +                        (rev, author, date, linecount) = map(lambda x: x.strip(), fields)
> +                        rev = rev[1:]  # strip off leaing 'r'
> +                        state = 'in_logentry'
> +
> +    def __contains__(self, key):
> +        return str(key) in self.comments
> +    def __getitem__(self, key):
> +        "Emulate dictionary, for new-style interface."
> +        return self.comments[str(key)]
> +
> +def isotime(s):
> +    "ISO 8601 to local clock time."
> +    if s[-1] == "Z":
> +        s = s[:-1]
> +    if "." in s:
> +        (date, msec) = s.split(".")
> +    else:
> +        date = s
> +        msec = "0"
> +    # Note: no leap-second correction!
> +    return calendar.timegm(time.strptime(date, "%Y-%m-%dT%H:%M:%S")) + float("0." + msec)
> +
> +def reference_mapper(value, mutator, flagrefs=False):
> +    "Apply a mutator function to revision references."
> +    revrefs = []
> +    for matchobj in re.finditer(r'\br([0-9]+)\b', value):
> +        revrefs.append(matchobj)
> +    if revrefs:
> +        revrefs.reverse()
> +        for m in revrefs:
> +            new = mutator(m.group(1))
> +            if flagrefs:
> +                new = "{" + new + "}"
> +            if new != m.group(1):
> +                value = value[:m.start(1)] + new + value[m.end(1):]
> +    return value
> +
> +# Generic machinery ends here, actual command implementations begin
> +
> +def squash(source, timefuzz,
> +           mapto=None, selection=None, excise=None,
> +           flagrefs=False, compressmap=False):
> +    "Coalesce adjacent commits with same author+log and close timestamps."
> +    dupes = []
> +    # The tricky bit is rewriting the revision numbers in node headers
> +    # associated with copy actions.
> +    clique_map = {}    # Map revisions to the base reves of their cliques
> +    squash_map = {}    # Map clique bases revs to their squashed numbers
> +    skipcount = numbered = clique_base = 0
> +    outmap = []
> +    def hacklog(propkeys, propdict, revision):
> +        # Hack references to revision levels in comments.
> +        for (key, value) in propdict.items():
> +            if key == "svn:log":
> +                propdict[key] = reference_mapper(value, lambda old: str(squash_map[clique_map[int(old)]]), flagrefs)
> +        return (propkeys, propdict)
> +    prevprops = {"svn:log":"", "svn:author":"", "svn:date":0}
> +    omit = excise is not None and 0 in excise
> +    while True:
> +        stash = source.read_until_next("Revision-number:", clique_map)
> +        if not omit:
> +            sys.stdout.write(stash)
> +        if not source.has_line_buffered():
> +            if excise is not None and dupes and dupes[0] in excise:
> +                outmap.append((None, dupes))
> +            elif numbered >= 1:
> +                outmap.append((numbered-1, dupes))
> +            break
> +        else:
> +            (revision, stash, properties) = source.read_revision_header(hacklog)
> +            # We have all properties of this revision.
> +            # Compute whether to merge it with the previous one.
> +            skip = "svn:log" in properties and "svn:author" in properties \
> +                   and properties["svn:log"] == prevprops.get("svn:log") \
> +                   and properties["svn:author"] == prevprops.get("svn:author") \
> +                   and (selection is None or revision in selection) \
> +                   and abs(isotime(properties["svn:date"]) - isotime(prevprops.get("svn:date"))) < timefuzz
> +            # Did user request an unconditional omission?
> +            omit = excise is not None and revision in excise
> +            if skip and omit:
> +                sys.stderr.write("squash: can't omit a revision about to be squashed.\n")
> +                sys.exit(1)
> +            # Treat spans of omitted commits as cliques for reporting
> +            if omit and excise is not None and revision-1 in excise:
> +                skip = True
> +            # The magic moment
> +            if skip:
> +                skipcount += 1
> +                clique_map[revision] = clique_base
> +            else:
> +                clique_base = revision
> +                clique_map[clique_base] = clique_base
> +                squash_map[clique_base] = revision - skipcount
> +                if excise is not None and dupes and dupes[0] in excise:
> +                    outmap.append((None, dupes))
> +                elif numbered >= 1:
> +                    outmap.append((numbered-1, dupes))
> +                dupes = []
> +                if omit:
> +                    skipcount += 1
> +                else:
> +                    sys.stdout.write(stash)
> +                    prevprops = properties
> +                    numbered += 1
> +            dupes.append(revision)
> +        # Go back around to copying to the next revision header.
> +    if mapto:
> +        mapto.write(("%% %d out of %d original revisions squashed, leaving %d" \
> +                     % (skipcount, revision, numbered-1)) + os.linesep)
> +        if not compressmap:
> +            for (numbered, dupes) in outmap:
> +                if numbered is None:
> +                    mapto.write("  None <- " + " ".join(map(str, dupes))+os.linesep)
> +                else:
> +                    mapto.write(("%6d <- " % numbered) + " ".join(map(str, dupes))+os.linesep)
> +        else:
> +            compressed = []
> +            force_new_range = True
> +            last_n = -1
> +            last_oldrevs = []
> +            # Process the raw outmap into a form that compressees ranges.
> +            # Squash cliques are left alone.  Ranger between
> +            # them map to either
> +            # (1) None followed by a singleton list (single deleted rev)
> +            # (2) None followed by a two-element list (range of deletions)
> +            # (3) Single number followed by singleton list = 1-element range)
> +            # (4) Two-element list followed by two-element list =
> +            #     multiple elements, old range to new range.
> +            for (n, oldrevs) in outmap:
> +                #print >>sys.stderr, "I see:", (n, oldrevs)
> +                cliquebase = oldrevs[0]
> +                if len(oldrevs) > 1:
> +                    compressed.append((n, oldrevs))
> +                    force_new_range = True
> +                else:
> +                    if (n is None) != (last_n is None):
> +                        #print >>sys.stderr, "Forcing range break"
> +                        force_new_range = True
> +                    if force_new_range:
> +                        compressed.append((n, oldrevs))
> +                    else:
> +                        #print >>sys.stderr, "Adding to range"
> +                        if len(last_oldrevs) == 1:
> +                            oldrevs = last_oldrevs + oldrevs
> +                        else:
> +                            oldrevs = last_oldrevs[:1] + oldrevs
> +                        lowerbound = compressed[-1][0]
> +                        if (last_n is None) and (n is None):
> +                            compressed[-1] = [None, oldrevs]
> +                        elif type(lowerbound) == type(0):
> +                            compressed[-1] = [[lowerbound, n], oldrevs]
> +                        else:
> +                            compressed[-1] = [lowerbound[:1] + [n], oldrevs]
> +                    force_new_range = False
> +                    last_n = n
> +                    last_oldrevs = oldrevs
> +            #print >>sys.stderr, "Compressed:", compressed
> +            for (a, b) in compressed:
> +                if a is None:
> +                    if len(b) == 1:
> +                        print >>mapto, "  None         <- %d" % b[0]
> +                        continue
> +                    else:
> +                        print >>mapto, "  None         <- %d..%d" % (b[0], b[-1])
> +                        continue
> +                else:
> +                    if type(a) == type(0) and len(b) == 1:
> +                        print >>mapto, "%6d         <- %d" % (a, b[0])
> +                        continue
> +                    elif type(a) == type(0) and type(b) == type([]):
> +                        print >>mapto, "%6d         <- %d..%d" % (a, b[0], b[-1])
> +                        continue
> +                    elif type(a) == type([]) and len(a)==2 and len(b)==2:
> +                        print >>mapto, "%6d..%-6d <- %d..%d" % (a[0], a[1], b[0], b[1])
> +                        continue
> +                sys.stderr.write("svncutter: Internal error on %s\n" % ((a, b),))
> +                sys.exit(1)
> +
> +def select(source, selection):
> +    "Select a portion of the dump file defined by a revision selection."
> +    emit = 0 in selection
> +    while True:
> +        stash = source.read_until_next("Revision-number:")
> +        if emit:
> +            sys.stdout.write(stash)
> +        if not source.has_line_buffered():
> +            return
> +        else:
> +            revision = int(source.linebuffer.split()[1])
> +            if revision in selection:
> +                sys.stdout.write(source.flush())
> +                emit = True
> +            elif revision == selection.upperbound()+1:
> +                return
> +            else:
> +                source.flush()
> +
> +def propdel(source, properties, selection):
> +    "Delete unversioned revision properties."
> +    def delhook(propkeys, propdict, revision):
> +        for propname in properties:
> +            if propname in propdict:
> +                del propdict[propname]
> +        return (propkeys, propdict)
> +    source.apply_property_hook(selection, delhook)
> +
> +def propset(source, properties, selection):
> +    "Set unversioned revision properties."
> +    def sethook(propkeys, propdict, revision):
> +        for prop in properties:
> +            (propname, propval) = prop.split("=")
> +            if propname in propdict:
> +                propdict[propname] = propval
> +        return (propkeys, propdict)
> +    source.apply_property_hook(selection, sethook)
> +
> +def proprename(source, properties, selection):
> +    "Rename unversioned revision properties."
> +    def renamehook(propkeys, propdict, revision):
> +        for prop in properties:
> +            (oldname, newname) = prop.split("->")
> +            if oldname in propdict:
> +                propdict[newname] = propdict[oldname]
> +                del propdict[oldname]
> +        return (propkeys, propdict)
> +    source.apply_property_hook(selection, renamehook)
> +
> +def log(source, selection):
> +    "Extract log entries."
> +    while True:
> +        source.read_until_next("Revision-number:")
> +        if not source.has_line_buffered():
> +            return
> +        else:
> +            (revision, stash, props) = source.read_revision_header()
> +            logentry = props.get("svn:log")
> +            if logentry:
> +                print "-" * 72
> +                author = props.get("svn:author", "(no author)")
> +                date = props["svn:date"].split(".")[0]
> +                date = time.strptime(date, "%Y-%m-%dT%H:%M:%S")
> +                date = time.strftime("%Y-%m-%d %H:%M:%S +0000 (%a, %d %b %Y)", date)
> +                print "r%s | %s | %s | %d lines" % (revision,
> +                                                    author,
> +                                                    date,
> +                                                    logentry.count(os.linesep))
> +                sys.stdout.write("\n" + logentry + "\n")
> +
> +def setlog(source, logpatch, selection):
> +    "Mutate log entries."
> +    logpatch = Logfile(file(logpatch), selection)
> +    def loghook(propkeys, propdict, revision):
> +        if "svn:log" in propkeys and revision in logpatch:
> +            (author, date, logentry) = logpatch[revision]
> +            if author != propdict.get("svn:author", "(no author)"):
> +                sys.stderr.write("svncutter: author of revision %s doesn't look right, aborting!\n" % revision)
> +                sys.exit(1)
> +            propdict["svn:log"] = logentry
> +        return (propkeys, propdict)
> +    source.apply_property_hook(selection, loghook)
> +
> +if __name__ == '__main__':
> +    try:
> +        (options, arguments) = getopt.getopt(sys.argv[1:], "ce:fl:m:p:qr:",
> +                                             ["excise", "flagrefs", "revprop=",
> +                                              "logpatch=", "map=",
> +                                              "quiet", "range="])
> +        selection = SubversionRange("0:HEAD")
> +        timefuzz = 300 # 5 minute fuzz
> +        compressmap = False
> +        excise = None
> +        revprops = []
> +        progress = True
> +        flagrefs = False
> +        logpatch = None
> +        mapto = None
> +        for (switch, val) in options:
> +            if switch in ('-c', '--compressmap'):
> +                compressmap = True
> +            elif switch in ('-e', '--excise'):
> +                excise = SubversionRange(val)
> +            elif switch in ('-f', '--flagrefs'):
> +                flagrefs = True
> +            elif switch in ('-l', '--logentries'):
> +                logpatch = val
> +            elif switch in ('-m', '--map'):
> +                mapto = open(val, "w")
> +            elif switch in ('-p', '--revprop'):
> +                revprops.append(val)
> +            elif switch in ('-q', '--quiet'):
> +                progress = False
> +            elif switch in ('-r', '--range'):
> +                selection = SubversionRange(val)
> +        if len(arguments) == 0:
> +            sys.stderr.write("Type 'svncutter help' for usage." + os.linesep)
> +            sys.exit(1)
> +        baton = None
> +        #if arguments[0] != 'help':
> +        #    if progress:
> +        #        baton = Baton(oneliners[arguments[0]], "done")
> +        #    else:
> +        #        baton = None
> +        if arguments[0] == "squash":
> +            squash(DumpfileSource(sys.stdin, baton),
> +                   timefuzz, mapto, selection, excise, flagrefs, compressmap)
> +        elif arguments[0] == "propdel":
> +            if not revprops:
> +                sys.stderr.write("svncutter: propdel requires one or more --revprop options.\n")
> +            if progress:
> +                baton = Baton("", "done")
> +            else:
> +                baton = None
> +            propdel(DumpfileSource(sys.stdin, baton), revprops, selection)
> +        elif arguments[0] == "propset":
> +            if not revprops:
> +                sys.stderr.write("svncutter: propset requires one or move --revprop options.\n")
> +            propset(DumpfileSource(sys.stdin, baton), revprops, selection)
> +        elif arguments[0] == "proprename":
> +            if not revprops:
> +                sys.stderr.write("svncutter: proprename requires one or move --revprop options.\n")
> +            propset(DumpfileSource(sys.stdin, baton), revprops, selection)
> +        elif arguments[0] == "select":
> +            select(DumpfileSource(sys.stdin, baton), selection)
> +        elif arguments[0] == "log":
> +            log(DumpfileSource(sys.stdin, baton), selection)
> +        elif arguments[0] == "setlog":
> +            if not logpatch:
> +                sys.stderr.write("svncutter: setlog requires a log entries file.\n")
> +            setlog(DumpfileSource(sys.stdin, baton), logpatch, selection)
> +        elif arguments[0] == "help":
> +            if len(arguments) == 1:
> +                sys.stdout.write(__doc__)
> +            else:
> +                sys.stdout.write(helpdict.get(arguments[1], arguments[1] + ": no such subcommand.\n"))
> +        else:
> +            sys.stderr.write(('"%s": unknown subcommand\n' % arguments[0])+os.linesep)
> +            sys.exit(1)
> +    except KeyboardInterrupt:
> +        pass
> +
> +# script ends here
>
> ------------------------------------------------------
> http://subversion.tigris.org/ds/viewMessage.do?dsForumId=495&dsMessageId=2405326
>

------------------------------------------------------
http://subversion.tigris.org/ds/viewMessage.do?dsForumId=462&dsMessageId=2406520
Received on 2009-10-12 08:01:46 CEST

This is an archived mail posted to the Subversion Dev mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.