[Pkg-bazaar-commits] ./bzr/unstable r757: - add john's changeset plugin

Martin Pool mbp at sourcefrog.net
Fri Apr 10 08:20:57 UTC 2009


------------------------------------------------------------
revno: 757
committer: Martin Pool <mbp at sourcefrog.net>
timestamp: Wed 2005-06-22 19:08:43 +1000
message:
  - add john's changeset plugin
added:
  contrib/plugins/
  contrib/plugins/changeset/
  contrib/plugins/changeset/__init__.py
  contrib/plugins/changeset/apply_changeset.py
  contrib/plugins/changeset/common.py
  contrib/plugins/changeset/gen_changeset.py
  contrib/plugins/changeset/read_changeset.py
-------------- next part --------------
=== added directory 'contrib/plugins'
=== added directory 'contrib/plugins/changeset'
=== added file 'contrib/plugins/changeset/__init__.py'
--- a/contrib/plugins/changeset/__init__.py	1970-01-01 00:00:00 +0000
+++ b/contrib/plugins/changeset/__init__.py	2005-06-22 09:08:43 +0000
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+"""\
+This is an attempt to take the internal delta object, and represent
+it as a single-file text-only changeset.
+This should have commands for both generating a changeset,
+and for applying a changeset.
+"""
+
+import bzrlib, bzrlib.commands
+
+class cmd_changeset(bzrlib.commands.Command):
+    """Generate a bundled up changeset.
+
+    This changeset contains all of the meta-information of a
+    diff, rather than just containing the patch information.
+
+    Right now, rollup changesets, or working tree changesets are
+    not supported. This will only generate a changeset that has been
+    committed. You can use "--revision" to specify a certain change
+    to display.
+    """
+    takes_options = ['revision', 'diff-options']
+    takes_args = ['file*']
+    aliases = ['cset']
+
+    def run(self, revision=None, file_list=None, diff_options=None):
+        from bzrlib import find_branch
+        import gen_changeset
+        import sys
+
+        if isinstance(revision, (list, tuple)):
+            if len(revision) > 1:
+                raise BzrCommandError('We do not support rollup-changesets yet.')
+            revision = revision[0]
+        if file_list:
+            b = find_branch(file_list[0])
+            file_list = [b.relpath(f) for f in file_list]
+            if file_list == ['']:
+                # just pointing to top-of-tree
+                file_list = None
+        else:
+            b = find_branch('.')
+
+        gen_changeset.show_changeset(b, revision,
+                specific_files=file_list,
+                external_diff_options=diff_options,
+                to_file=sys.stdout)
+
+class cmd_verify_changeset(bzrlib.commands.Command):
+    """Read a written changeset, and make sure it is valid.
+
+    """
+    takes_args = ['filename?']
+
+    def run(self, filename=None):
+        import sys, read_changeset
+        if filename is None or filename == '-':
+            f = sys.stdin
+        else:
+            f = open(filename, 'rb')
+
+        cset_info = read_changeset.read_changeset(f)
+        print cset_info
+        cset = cset_info.get_changeset()
+        print cset.entries
+
+class cmd_apply_changeset(bzrlib.commands.Command):
+    """Read in the given changeset, and apply it to the
+    current tree.
+
+    """
+    takes_args = ['filename?']
+    takes_options = []
+
+    def run(self, filename=None, reverse=False, auto_commit=False):
+        from bzrlib import find_branch
+        import sys
+        import apply_changeset
+
+        b = find_branch('.') # Make sure we are in a branch
+        if filename is None or filename == '-':
+            f = sys.stdin
+        else:
+            f = open(filename, 'rb')
+
+        apply_changeset.apply_changeset(b, f, reverse=reverse,
+                auto_commit=auto_commit)
+
+
+if hasattr(bzrlib.commands, 'register_plugin_cmd'):
+    bzrlib.commands.register_plugin_cmd(cmd_changeset)
+    bzrlib.commands.register_plugin_cmd(cmd_verify_changeset)
+    bzrlib.commands.register_plugin_cmd(cmd_apply_changeset)
+
+    bzrlib.commands.OPTIONS['reverse'] = None
+    bzrlib.commands.OPTIONS['auto-commit'] = None
+    cmd_apply_changeset.takes_options.append('reverse')
+    cmd_apply_changeset.takes_options.append('auto-commit')
+

=== added file 'contrib/plugins/changeset/apply_changeset.py'
--- a/contrib/plugins/changeset/apply_changeset.py	1970-01-01 00:00:00 +0000
+++ b/contrib/plugins/changeset/apply_changeset.py	2005-06-22 09:08:43 +0000
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+"""\
+This contains the apply changset function for bzr
+"""
+
+import bzrlib
+
+def apply_changeset(branch, from_file, reverse=False, auto_commit=False):
+    from bzrlib.changeset import apply_changeset as _apply_changeset
+    from bzrlib.merge import regen_inventory
+    import sys, read_changeset
+
+
+    cset_info = read_changeset.read_changeset(from_file)
+    cset = cset_info.get_changeset()
+    inv = {}
+    for file_id in branch.inventory:
+        inv[file_id] = branch.inventory.id2path(file_id)
+    changes = _apply_changeset(cset, inv, branch.base,
+            reverse=reverse)
+
+    adjust_ids = []
+    for id, path in changes.iteritems():
+        if path is not None:
+            if path == '.':
+                path = ''
+        adjust_ids.append((path, id))
+
+    branch.set_inventory(regen_inventory(branch, branch.base, adjust_ids))
+
+    if auto_commit:
+        from bzrlib.commit import commit
+        if branch.last_patch() == cset_info.precursor:
+            # This patch can be applied directly
+            commit(branch, message = cset_info.message,
+                    timestamp=float(cset_info.timestamp),
+                    timezone=float(cset_info.timezone),
+                    committer=cset_info.committer,
+                    rev_id=cset_info.revision)
+
+

=== added file 'contrib/plugins/changeset/common.py'
--- a/contrib/plugins/changeset/common.py	1970-01-01 00:00:00 +0000
+++ b/contrib/plugins/changeset/common.py	2005-06-22 09:08:43 +0000
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+"""\
+Common entries, like strings, etc, for the changeset reading + writing code.
+"""
+
+header_str = 'Bazaar-NG (bzr) changeset v'
+version = (0, 0, 5)
+
+def get_header():
+    return [
+        header_str + '.'.join([str(v) for v in version]),
+        'This changeset can be applied with bzr apply-changeset',
+        ''
+    ]
+

=== added file 'contrib/plugins/changeset/gen_changeset.py'
--- a/contrib/plugins/changeset/gen_changeset.py	1970-01-01 00:00:00 +0000
+++ b/contrib/plugins/changeset/gen_changeset.py	2005-06-22 09:08:43 +0000
@@ -0,0 +1,345 @@
+#!/usr/bin/env python
+"""\
+Just some work for generating a changeset.
+"""
+
+import bzrlib, bzrlib.errors
+
+import common
+
+from bzrlib.inventory import ROOT_ID
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+def _canonicalize_revision(branch, revno):
+    """Turn some sort of revision information into a single
+    set of from-to revision ids.
+
+    A revision id can be None if there is no associated revison.
+
+    :return: (old, new)
+    """
+    # This is a little clumsy because revision parsing may return
+    # a single entry, or a list
+    if revno is None:
+        new = branch.last_patch()
+    else:
+        new = branch.lookup_revision(revno)
+
+    if new is None:
+        raise BzrCommandError('Cannot generate a changset with no commits in tree.')
+
+    old = branch.get_revision(new).precursor
+
+    return old, new
+
+def _get_trees(branch, revisions):
+    """Get the old and new trees based on revision.
+    """
+    from bzrlib.tree import EmptyTree
+    if revisions[0] is None:
+        if hasattr(branch, 'get_root_id'): # Watch out for trees with labeled ROOT ids
+            old_tree = EmptyTree(branch.get_root_id) 
+        else:
+            old_tree = EmptyTree()
+    else:
+        old_tree = branch.revision_tree(revisions[0])
+
+    if revisions[1] is None:
+        # This is for the future, once we support rollup revisions
+        # Or working tree revisions
+        new_tree = branch.working_tree()
+    else:
+        new_tree = branch.revision_tree(revisions[1])
+    return old_tree, new_tree
+
+def _fake_working_revision(branch):
+    """Fake a Revision object for the working tree.
+    
+    This is for the future, to support changesets against the working tree.
+    """
+    from bzrlib.revision import Revision
+    import time
+    from bzrlib.osutils import local_time_offset, \
+            username
+
+    precursor = branch.last_patch()
+    precursor_sha1 = branch.get_revision_sha1(precursor)
+
+    return Revision(timestamp=time.time(),
+            timezone=local_time_offset(),
+            committer=username(),
+            precursor=precursor,
+            precursor_sha1=precursor_sha1)
+
+
+class MetaInfoHeader(object):
+    """Maintain all of the header information about this
+    changeset.
+    """
+
+    def __init__(self, branch, revisions, delta,
+            full_remove=True, full_rename=False,
+            external_diff_options = None,
+            new_tree=None, old_tree=None,
+            old_label = '', new_label = ''):
+        """
+        :param full_remove: Include the full-text for a delete
+        :param full_rename: Include an add+delete patch for a rename
+        """
+        self.branch = branch
+        self.delta = delta
+        self.full_remove=full_remove
+        self.full_rename=full_rename
+        self.external_diff_options = external_diff_options
+        self.old_label = old_label
+        self.new_label = new_label
+        self.old_tree = old_tree
+        self.new_tree = new_tree
+        self.to_file = None
+        self.revno = None
+        self.precursor_revno = None
+
+        self._get_revision_list(revisions)
+
+    def _get_revision_list(self, revisions):
+        """This generates the list of all revisions from->to.
+
+        This is for the future, when we support having a rollup changeset.
+        For now, the list should only be one long.
+        """
+        old_revno = None
+        new_revno = None
+        rh = self.branch.revision_history()
+        for revno, rev in enumerate(rh):
+            if rev == revisions[0]:
+                old_revno = revno
+            if rev == revisions[1]:
+                new_revno = revno
+
+        self.revision_list = []
+        if old_revno is None:
+            self.base_revision = None # Effectively the EmptyTree()
+            old_revno = -1
+        else:
+            self.base_revision = self.branch.get_revision(rh[old_revno])
+        if new_revno is None:
+            # For the future, when we support working tree changesets.
+            for rev_id in rh[old_revno+1:]:
+                self.revision_list.append(self.branch.get_revision(rev_id))
+            self.revision_list.append(_fake_working_revision(self.branch))
+        else:
+            for rev_id in rh[old_revno+1:new_revno+1]:
+                self.revision_list.append(self.branch.get_revision(rev_id))
+        self.precursor_revno = old_revno
+        self.revno = new_revno
+
+    def _write(self, txt, key=None):
+        if key:
+            self.to_file.write('# %s: %s\n' % (key, txt))
+        else:
+            self.to_file.write('# %s\n' % (txt,))
+
+    def write_meta_info(self, to_file):
+        """Write out the meta-info portion to the supplied file.
+
+        :param to_file: Write out the meta information to the supplied
+                        file
+        """
+        self.to_file = to_file
+
+        self._write_header()
+        self._write_diffs()
+        self._write_footer()
+
+    def _write_header(self):
+        """Write the stuff that comes before the patches."""
+        from bzrlib.osutils import username, format_date
+        write = self._write
+
+        for line in common.get_header():
+            write(line)
+
+        # This grabs the current username, what we really want is the
+        # username from the actual patches.
+        #write(username(), key='committer')
+        assert len(self.revision_list) == 1
+        rev = self.revision_list[0]
+        write(rev.committer, key='committer')
+        write(format_date(rev.timestamp, offset=rev.timezone), key='date')
+        write(str(self.revno), key='revno')
+        if rev.message:
+            self.to_file.write('# message:\n')
+            for line in rev.message.split('\n'):
+                self.to_file.write('#    %s\n' % line)
+        write(rev.revision_id, key='revision')
+
+        if self.base_revision:
+            write(self.base_revision.revision_id, key='precursor')
+            write(str(self.precursor_revno), key='precursor revno')
+
+
+        write('')
+        self.to_file.write('\n')
+
+    def _write_footer(self):
+        """Write the stuff that comes after the patches.
+
+        This is meant to be more meta-information, which people probably don't want
+        to read, but which is required for proper bzr operation.
+        """
+        write = self._write
+
+        write('BEGIN BZR FOOTER')
+
+        assert len(self.revision_list) == 1 # We only handle single revision entries
+        rev = self.revision_list[0]
+        write(self.branch.get_revision_sha1(rev.revision_id),
+                key='revision sha1')
+        if self.base_revision:
+            rev_id = self.base_revision.revision_id
+            write(self.branch.get_revision_sha1(rev_id),
+                    key='precursor sha1')
+
+        write('%.9f' % rev.timestamp, key='timestamp')
+        write(str(rev.timezone), key='timezone')
+
+        self._write_ids()
+
+        write('END BZR FOOTER')
+
+    def _write_revisions(self):
+        """Not used. Used for writing multiple revisions."""
+        first = True
+        for rev in self.revision_list:
+            if rev.revision_id is not None:
+                if first:
+                    self._write('revisions:')
+                    first = False
+                self._write(' '*4 + rev.revision_id + '\t' + self.branch.get_revision_sha1(rev.revision_id))
+
+
+    def _write_ids(self):
+        if hasattr(self.branch, 'get_root_id'):
+            root_id = self.branch.get_root_id()
+        else:
+            root_id = ROOT_ID
+
+        old_ids = set()
+        new_ids = set()
+
+        for path, file_id, kind in self.delta.removed:
+            old_ids.add(file_id)
+        for path, file_id, kind in self.delta.added:
+            new_ids.add(file_id)
+        for old_path, new_path, file_id, kind, text_modified in self.delta.renamed:
+            old_ids.add(file_id)
+            new_ids.add(file_id)
+        for path, file_id, kind in self.delta.modified:
+            new_ids.add(file_id)
+
+        self._write(root_id, key='tree root id')
+
+        def write_ids(tree, id_set, name):
+            if len(id_set) > 0:
+                self.to_file.write('# %s ids:\n' % name)
+            seen_ids = set([root_id])
+            while len(id_set) > 0:
+                file_id = id_set.pop()
+                if file_id in seen_ids:
+                    continue
+                seen_ids.add(file_id)
+                ie = tree.inventory[file_id]
+                if ie.parent_id not in seen_ids:
+                    id_set.add(ie.parent_id)
+                path = tree.inventory.id2path(file_id)
+                self.to_file.write('#    %s\t%s\t%s\n'
+                        % (path.encode('utf8'), file_id.encode('utf8'),
+                            ie.parent_id.encode('utf8')))
+        write_ids(self.new_tree, new_ids, 'file')
+        write_ids(self.old_tree, old_ids, 'old file')
+
+    def _write_diffs(self):
+        """Write out the specific diffs"""
+        from bzrlib.diff import internal_diff, external_diff
+        DEVNULL = '/dev/null'
+
+        if self.external_diff_options:
+            assert isinstance(self.external_diff_options, basestring)
+            opts = self.external_diff_options.split()
+            def diff_file(olab, olines, nlab, nlines, to_file):
+                external_diff(olab, olines, nlab, nlines, to_file, opts)
+        else:
+            diff_file = internal_diff
+
+        for path, file_id, kind in self.delta.removed:
+            print >>self.to_file, '*** removed %s %r' % (kind, path)
+            if kind == 'file' and self.full_remove:
+                diff_file(self.old_label + path,
+                          self.old_tree.get_file(file_id).readlines(),
+                          DEVNULL, 
+                          [],
+                          self.to_file)
+    
+        for path, file_id, kind in self.delta.added:
+            print >>self.to_file, '*** added %s %r' % (kind, path)
+            if kind == 'file':
+                diff_file(DEVNULL,
+                          [],
+                          self.new_label + path,
+                          self.new_tree.get_file(file_id).readlines(),
+                          self.to_file)
+    
+        for old_path, new_path, file_id, kind, text_modified in self.delta.renamed:
+            print >>self.to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
+            if self.full_rename and kind == 'file':
+                diff_file(self.old_label + old_path,
+                          self.old_tree.get_file(file_id).readlines(),
+                          DEVNULL, 
+                          [],
+                          self.to_file)
+                diff_file(DEVNULL,
+                          [],
+                          self.new_label + new_path,
+                          self.new_tree.get_file(file_id).readlines(),
+                          self.to_file)
+            elif text_modified:
+                    diff_file(self.old_label + old_path,
+                              self.old_tree.get_file(file_id).readlines(),
+                              self.new_label + new_path,
+                              self.new_tree.get_file(file_id).readlines(),
+                              self.to_file)
+    
+        for path, file_id, kind in self.delta.modified:
+            print >>self.to_file, '*** modified %s %r' % (kind, path)
+            if kind == 'file':
+                diff_file(self.old_label + path,
+                          self.old_tree.get_file(file_id).readlines(),
+                          self.new_label + path,
+                          self.new_tree.get_file(file_id).readlines(),
+                          self.to_file)
+
+def show_changeset(branch, revision=None, specific_files=None,
+        external_diff_options=None, to_file=None,
+        include_full_diff=False):
+    from bzrlib.diff import compare_trees
+
+    if to_file is None:
+        import sys
+        to_file = sys.stdout
+    revisions = _canonicalize_revision(branch, revision)
+
+    old_tree, new_tree = _get_trees(branch, revisions)
+
+    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
+                          specific_files=specific_files)
+
+    meta = MetaInfoHeader(branch, revisions, delta,
+            external_diff_options=external_diff_options,
+            old_tree=old_tree, new_tree=new_tree)
+    meta.write_meta_info(to_file)
+
+

=== added file 'contrib/plugins/changeset/read_changeset.py'
--- a/contrib/plugins/changeset/read_changeset.py	1970-01-01 00:00:00 +0000
+++ b/contrib/plugins/changeset/read_changeset.py	2005-06-22 09:08:43 +0000
@@ -0,0 +1,342 @@
+#!/usr/bin/env python
+"""\
+Read in a changeset output, and process it into a Changeset object.
+"""
+
+import bzrlib, bzrlib.changeset
+import common
+
+class BadChangeset(Exception): pass
+class MalformedHeader(BadChangeset): pass
+class MalformedPatches(BadChangeset): pass
+class MalformedFooter(BadChangeset): pass
+
+def _unescape(name):
+    """Now we want to find the filename effected.
+    Unfortunately the filename is written out as
+    repr(filename), which means that it surrounds
+    the name with quotes which may be single or double
+    (single is preferred unless there is a single quote in
+    the filename). And some characters will be escaped.
+
+    TODO:   There has to be some pythonic way of undo-ing the
+            representation of a string rather than using eval.
+    """
+    delimiter = name[0]
+    if name[-1] != delimiter:
+        raise BadChangeset('Could not properly parse the'
+                ' filename: %r' % name)
+    # We need to handle escaped hexadecimals too.
+    return name[1:-1].replace('\"', '"').replace("\'", "'")
+
+class ChangesetInfo(object):
+    """This is the intermediate class that gets filled out as
+    the file is read.
+    """
+    def __init__(self):
+        self.committer = None
+        self.date = None
+        self.message = None
+        self.revno = None
+        self.revision = None
+        self.revision_sha1 = None
+        self.precursor = None
+        self.precursor_sha1 = None
+        self.precursor_revno = None
+
+        self.timestamp = None
+        self.timezone = None
+
+        self.tree_root_id = None
+        self.file_ids = None
+        self.old_file_ids = None
+
+        self.actions = [] #this is the list of things that happened
+        self.id2path = {} # A mapping from file id to path name
+        self.path2id = {} # The reverse mapping
+        self.id2parent = {} # A mapping from a given id to it's parent id
+
+        self.old_id2path = {}
+        self.old_path2id = {}
+        self.old_id2parent = {}
+
+    def __str__(self):
+        import pprint
+        return pprint.pformat(self.__dict__)
+
+    def create_maps(self):
+        """Go through the individual id sections, and generate the 
+        id2path and path2id maps.
+        """
+        # Rather than use an empty path, the changeset code seems 
+        # to like to use "./." for the tree root.
+        self.id2path[self.tree_root_id] = './.'
+        self.path2id['./.'] = self.tree_root_id
+        self.id2parent[self.tree_root_id] = bzrlib.changeset.NULL_ID
+        self.old_id2path = self.id2path.copy()
+        self.old_path2id = self.path2id.copy()
+        self.old_id2parent = self.id2parent.copy()
+
+        if self.file_ids:
+            for info in self.file_ids:
+                path, f_id, parent_id = info.split('\t')
+                self.id2path[f_id] = path
+                self.path2id[path] = f_id
+                self.id2parent[f_id] = parent_id
+        if self.old_file_ids:
+            for info in self.old_file_ids:
+                path, f_id, parent_id = info.split('\t')
+                self.old_id2path[f_id] = path
+                self.old_path2id[path] = f_id
+                self.old_id2parent[f_id] = parent_id
+
+    def get_changeset(self):
+        """Create a changeset from the data contained within."""
+        from bzrlib.changeset import Changeset, ChangesetEntry, \
+            PatchApply, ReplaceContents
+        cset = Changeset()
+        
+        entry = ChangesetEntry(self.tree_root_id, 
+                bzrlib.changeset.NULL_ID, './.')
+        cset.add_entry(entry)
+        for info, lines in self.actions:
+            parts = info.split(' ')
+            action = parts[0]
+            kind = parts[1]
+            extra = ' '.join(parts[2:])
+            if action == 'renamed':
+                old_path, new_path = extra.split(' => ')
+                old_path = _unescape(old_path)
+                new_path = _unescape(new_path)
+
+                new_id = self.path2id[new_path]
+                old_id = self.old_path2id[old_path]
+                assert old_id == new_id
+
+                new_parent = self.id2parent[new_id]
+                old_parent = self.old_id2parent[old_id]
+
+                entry = ChangesetEntry(old_id, old_parent, old_path)
+                entry.new_path = new_path
+                entry.new_parent = new_parent
+                if lines:
+                    entry.contents_change = PatchApply(''.join(lines))
+            elif action == 'removed':
+                old_path = _unescape(extra)
+                old_id = self.old_path2id[old_path]
+                old_parent = self.old_id2parent[old_id]
+                entry = ChangesetEntry(old_id, old_parent, old_path)
+                entry.new_path = None
+                entry.new_parent = None
+                if lines:
+                    # Technically a removed should be a ReplaceContents()
+                    # Where you need to have the old contents
+                    # But at most we have a remove style patch.
+                    #entry.contents_change = ReplaceContents()
+                    pass
+            elif action == 'added':
+                new_path = _unescape(extra)
+                new_id = self.path2id[new_path]
+                new_parent = self.id2parent[new_id]
+                entry = ChangesetEntry(new_id, new_parent, new_path)
+                entry.path = None
+                entry.parent = None
+                if lines:
+                    # Technically an added should be a ReplaceContents()
+                    # Where you need to have the old contents
+                    # But at most we have an add style patch.
+                    #entry.contents_change = ReplaceContents()
+                    entry.contents_change = PatchApply(''.join(lines))
+            elif action == 'modified':
+                new_path = _unescape(extra)
+                new_id = self.path2id[new_path]
+                new_parent = self.id2parent[new_id]
+                entry = ChangesetEntry(new_id, new_parent, new_path)
+                entry.path = None
+                entry.parent = None
+                if lines:
+                    # Technically an added should be a ReplaceContents()
+                    # Where you need to have the old contents
+                    # But at most we have an add style patch.
+                    #entry.contents_change = ReplaceContents()
+                    entry.contents_change = PatchApply(''.join(lines))
+            else:
+                raise BadChangeset('Unrecognized action: %r' % action)
+            cset.add_entry(entry)
+        return cset
+
+class ChangesetReader(object):
+    """This class reads in a changeset from a file, and returns
+    a Changeset object, which can then be applied against a tree.
+    """
+    def __init__(self, from_file):
+        """Read in the changeset from the file.
+
+        :param from_file: A file-like object (must have iterator support).
+        """
+        object.__init__(self)
+        self.from_file = from_file
+        
+        self.info = ChangesetInfo()
+        # We put the actual inventory ids in the footer, so that the patch
+        # is easier to read for humans.
+        # Unfortunately, that means we need to read everything before we
+        # can create a proper changeset.
+        self._read_header()
+        next_line = self._read_patches()
+        if next_line is not None:
+            self._read_footer(next_line)
+
+    def get_info(self):
+        """Create the actual changeset object.
+        """
+        self.info.create_maps()
+        return self.info
+
+    def _read_header(self):
+        """Read the bzr header"""
+        header = common.get_header()
+        for head_line, line in zip(header, self.from_file):
+            if (line[:2] != '# '
+                    or line[-1] != '\n'
+                    or line[2:-1] != head_line):
+                raise MalformedHeader('Did not read the opening'
+                    ' header information.')
+
+        for line in self.from_file:
+            if self._handle_info_line(line) is not None:
+                break
+
+    def _handle_info_line(self, line, in_footer=False):
+        """Handle reading a single line.
+
+        This may call itself, in the case that we read_multi,
+        and then had a dangling line on the end.
+        """
+        # The bzr header is terminated with a blank line
+        # which does not start with #
+        next_line = None
+        if line[:1] == '\n':
+            return 'break'
+        if line[:2] != '# ':
+            raise MalformedHeader('Opening bzr header did not start with #')
+
+        line = line[2:-1] # Remove the '# '
+        if not line:
+            return # Ignore blank lines
+
+        if in_footer and line in ('BEGIN BZR FOOTER', 'END BZR FOOTER'):
+            return
+
+        loc = line.find(': ')
+        if loc != -1:
+            key = line[:loc]
+            value = line[loc+2:]
+            if not value:
+                value, next_line = self._read_many()
+        else:
+            if line[-1:] == ':':
+                key = line[:-1]
+                value, next_line = self._read_many()
+            else:
+                raise MalformedHeader('While looking for key: value pairs,'
+                        ' did not find the colon %r' % (line))
+
+        key = key.replace(' ', '_')
+        if hasattr(self.info, key):
+            if getattr(self.info, key) is None:
+                setattr(self.info, key, value)
+            else:
+                raise MalformedHeader('Duplicated Key: %s' % key)
+        else:
+            # What do we do with a key we don't recognize
+            raise MalformedHeader('Unknown Key: %s' % key)
+        
+        if next_line:
+            self._handle_info_line(next_line, in_footer=in_footer)
+
+    def _read_many(self):
+        """If a line ends with no entry, that means that it should be
+        followed with multiple lines of values.
+
+        This detects the end of the list, because it will be a line that
+        does not start with '#    '. Because it has to read that extra
+        line, it returns the tuple: (values, next_line)
+        """
+        values = []
+        for line in self.from_file:
+            if line[:5] != '#    ':
+                return values, line
+            values.append(line[5:-1])
+        return values, None
+
+    def _read_one_patch(self, first_line=None):
+        """Read in one patch, return the complete patch, along with
+        the next line.
+
+        :return: action, lines, next_line, do_continue
+        """
+        first = True
+        action = None
+
+        def parse_firstline(line):
+            if line[:1] == '#':
+                return None
+            if line[:3] != '***':
+                raise MalformedPatches('The first line of all patches'
+                    ' should be a bzr meta line "***"')
+            return line[4:-1]
+
+        if first_line is not None:
+            action = parse_firstline(first_line)
+            first = False
+            if action is None:
+                return None, [], first_line, False
+
+        lines = []
+        for line in self.from_file:
+            if first:
+                action = parse_firstline(line)
+                first = False
+                if action is None:
+                    return None, [], line, False
+            else:
+                if line[:3] == '***':
+                    return action, lines, line, True
+                elif line[:1] == '#':
+                    return action, lines, line, False
+                lines.append(line)
+        return action, lines, None, False
+            
+    def _read_patches(self):
+        next_line = None
+        do_continue = True
+        while do_continue:
+            action, lines, next_line, do_continue = \
+                    self._read_one_patch(next_line)
+            if action is not None:
+                self.info.actions.append((action, lines))
+        return next_line
+
+    def _read_footer(self, first_line=None):
+        """Read the rest of the meta information.
+
+        :param first_line:  The previous step iterates past what it
+                            can handle. That extra line is given here.
+        """
+        if first_line is not None:
+            if self._handle_info_line(first_line, in_footer=True) is not None:
+                return
+        for line in self.from_file:
+            if self._handle_info_line(line, in_footer=True) is not None:
+                break
+
+
+def read_changeset(from_file):
+    """Read in a changeset from a filelike object (must have "readline" support), and
+    parse it into a Changeset object.
+    """
+    cr = ChangesetReader(from_file)
+    info = cr.get_info()
+    return info
+



More information about the Pkg-bazaar-commits mailing list