Source

heechee / heechee / webdav / report.py

Full commit

import logging

from lxml.etree import tostring, Element, SubElement
from werkzeug.wrappers import Response
from werkzeug.exceptions import BadRequest
from urlparse import urlsplit

from heechee.constants import *
from heechee.treediff import *
from heechee.exceptions import *
from heechee.svndiff import make_cheap_diff
from heechee.repo import Directory, tree_get

class ReportMixin(object):
    
    """
    Mixin that provides the REPORT method.
    """
    
    def REPORT(self, request):
        "i.e. SVN-wants-a-massive-diff time."        
        # Can we use gzip?
        gzip_allowed = "gzip" in request.headers['Accept-Encoding']
        
        # See what kind of report they actually want
        tree = request.xml_body
        
        if tree.tag == SVN_NS+"update-report":
            
            # Get the revision they're starting at
            entry = tree.find(SVN_NS+"entry")
            source = int(entry.attrib['rev'])
            
            # Find what part of the path they want
            path_parts = urlsplit(tree.find(SVN_NS+"src-path").text)
            path = path_parts[2].strip('/')
            path_start = request.script_root.strip('/')
            path = path[len(path_start):].strip('/')
            
            # If they asked to start-empty, they want a new checkout
            if entry.attrib.get('start-empty', "false") == "true":
                source = 0
            
            # See if they asked for a target revision
            try:
                target = int(tree.find(SVN_NS+"target-revision").text)
            except AttributeError:
                # OK, they didn't - we're thus going for the top one
                target = self.repo.get_top_revision()
            
            logging.debug("report: %s -> %s" % (source, target))
            
            current_root = self.repo.tree_for_revision(source)
            new_root = self.repo.tree_for_revision(target)
            
            # If there's a subpath we're using, traverse down to that.
            if path:
                try:
                    current_root = tree_get(current_root, path.split("/"))
                except PathTraversalError:
                    # If we can't find the directory, it doesn't exist yet.
                    current_root = Directory(name=None, parent=None)
                try:
                    new_root = tree_get(new_root, path.split("/"))
                except PathTraversalError:
                    # Now, you can't target a non-existent directory!
                    return Response("Target directory does not exist.", status=404)
            
            changes = treediff(current_root, new_root)
            
            # Start off the update report
            up_rep = Element(SVN_NS+"update-report")
            up_rep.attrib['send-all'] = "true"
            SubElement(up_rep, SVN_NS+"target-revision", rev=str(target))
            
            # Make the root directory's entry.
            dir = SubElement(up_rep, SVN_NS+"open-directory", rev=str(target))
            dir_ch_in = SubElement(dir, DAV_NS+"checked-in")
            dir_ch_in.append(self._href(request.script_root + "/!svn/ver/%s/" % target))
            
            # Work out the sign of the change. This is used to ignore the
            # source revision in the change checking, below.
            change_sign = cmp(target - source, 0)
            
            # The queue is a list of (parent, item) tuples.
            if change_sign == 0:
                # We know there's no changes. Skip that loop.
                queue = []
            else:
                queue = [(dir, child) for child in changes.children]
            
            # Keep going over that queue, doing the actions.
            while queue:
                parent, node = queue.pop()
                
                # Is it an all-new directory?
                if isinstance(node, NewDirectory):
                    # Make the directory's entry
                    logging.debug("newdir %s" % node.item.get_path())
                    dir = SubElement(parent, SVN_NS+"add-directory", name=node.item.name, rev=str(target))
                    dir_ch_in = SubElement(dir, DAV_NS+"checked-in")
                    dir_ch_in.append(self._href(request.script_root + "/!svn/ver/%s%s" % (target, node.item.get_path())))
                    # Add its children
                    for child in node.children:
                        queue.append((dir, child))
                
                # An updated directory?
                elif isinstance(node, ChangedDirectory):
                    # Remember, it has to have changed!
                    if list(self.repo.file_changes(node.item.get_path(), source, target)):
                        # Make the directory's entry
                        logging.debug("chgdir %s" % node.item.get_path())
                        dir = SubElement(parent, SVN_NS+"open-directory", name=node.item.name, rev=str(target))
                        dir_ch_in = SubElement(dir, DAV_NS+"checked-in")
                        dir_ch_in.append(self._href(request.script_root + "/!svn/ver/%s%s" % (target, node.item.get_path())))
                        # Add its children
                        for child in node.children:
                            queue.append((dir, child))
                
                # An all-new file?
                elif isinstance(node, NewFile):
                    # Make the file object
                    logging.debug("newfile %s" % node.item.get_path())
                    file = SubElement(parent, SVN_NS+"add-file", name=node.item.name)
                    dir_ch_in = SubElement(file, DAV_NS+"checked-in")
                    dir_ch_in.append(self._href(request.script_root + "/!svn/ver/%s%s" % (target, node.item.get_path())))
                    rev_elem = SubElement(file, SVN_NS+"set-prop", name="svn:entry:committed-rev")
                    rev_elem.text = str(target)
                    txdelta = SubElement(file, SVN_NS+"txdelta")
                    txdelta.text = make_cheap_diff(node.item.contents).encode("base64")
                
                # An updated file?
                elif isinstance(node, ChangedFile):
                    # Remember, it has to have changed!
                    if list(self.repo.file_changes(node.item.get_path(), source, target)):
                        # Make the file object
                        logging.debug("chgfile %s" % node.item.get_path())
                        file = SubElement(parent, SVN_NS+"open-file", name=node.item.name, rev=str(target))
                        dir_ch_in = SubElement(file, DAV_NS+"checked-in")
                        dir_ch_in.append(self._href(request.script_root + "/!svn/ver/%s%s" % (target, node.item.get_path())))
                        rev_elem = SubElement(file, SVN_NS+"set-prop", name="svn:entry:committed-rev")
                        rev_elem.text = str(target)
                        txdelta = SubElement(file, SVN_NS+"txdelta")
                        txdelta.text = make_cheap_diff(node.item.contents).encode("base64")
                
                # A deleted file?
                elif isinstance(node, (DeletedFile, DeletedDirectory)):
                    # Make the file object
                    logging.debug("delfileordir %s" % node.item.get_path())
                    SubElement(parent, SVN_NS+"delete-entry", name=node.item.name, rev=str(target))
                
                # Fail.
                else:
                    raise ValueError("Unexpected item in changes queue: %s" % node)
            
            return Response(tostring(up_rep))
        
        elif tree.tag == SVN_NS + "log-report":
            assert "!svn" in request.path, "Log report on non-special directory!"
            
            # Get the start/end revision, and path
            start_rev = int(tree.find(SVN_NS+"start-revision").text)
            end_rev = int(tree.find(SVN_NS+"end-revision").text)
            
            parts = request.path.strip("/").split("/")
            revision = parts[2]
            base_path = "/".join(parts[3:])
            
            path = base_path + (tree.find(SVN_NS+"path").text or "")
            
            # Make sure the revs are the right way around
            if start_rev < end_rev:
                start_rev, end_rev = end_rev, start_rev
            
            # Send back the messages
            log_rep = Element(SVN_NS+"log-report")
            for info in reversed(list(self.repo.logs_for_revisions(start_rev, end_rev, path))):
                item = SubElement(log_rep, SVN_NS+"log-item")
                rev = SubElement(item, DAV_NS+"version-name")
                rev.text = str(info['rev'])
                comment = SubElement(item, DAV_NS+"comment")
                comment.text = info['comment']
                author = SubElement(item, DAV_NS+"creator-displayname")
                author.text = info['author']
                date = SubElement(item, SVN_NS+"date")
                date.text = info['date'].strftime("%Y-%m-%dT%H:%M:%S.00000Z")
            
            return Response(tostring(log_rep))
        
        else:
            return BadRequest()