Source

heechee / heechee / repo / git.py

Full commit
"""
Repository backend for Git.
"""

import os
import time

from ConfigParser import RawConfigParser
from heechee.repo import File, Directory, RepositoryBase
from heechee.exceptions import *
from dulwich import *


class Repository(RepositoryBase):
    
    uuid = "00000000-80c4-4579-810c-ce57b7db7bfe"
    
    def __init__(self, directory="."):
        super(Repository, self).__init__()
        self.directory = directory
        self.git_repo = repo.Repo(directory)
        self.repo_lineariser = RepositoryLineariser(self)
        self.repo_lineariser.add_missing(self.git_repo.head())
    
    
    ########### Public functions (main heechee repo API) ###########
    
    def get_top_revision(self):
        return self.repo_lineariser.top_revision_number()
    
    def tree_for_revision(self, revision):
        """
        Returns the entire tree for the given revision - including the
        top-level trunk/, tags/ and branches/ directories.
        `revision` is a Subversion revision.
        """
        
        revision = int(revision)
        
        # Root of the tree
        root = Directory(name=None, parent=None, rev=revision)
        
        # If they asked for the base revision, give them emptiness.
        if revision == 0:
            return root
        
        # Top-level dirs
        trunk = self._tree_for_commit(self.git_repo.commit(self.git_repo.head()), "trunk", root, revision)
        
        # Branches
        branches = Directory(name="branches", parent=root, rev=revision)
        
        # Tags
        tags = Directory(name="tags", parent=root, rev=revision)
        
        return root
    
    def file_changes(self, path, source, target):
        raise NotImplementedError
    
    def logs_for_revisions(self, highest, lowest, path):
        raise NotImplementedError
    
    def commit(self, branch_name, parent, message, author, changes, deletions):
        raise NotImplementedError
    
    def mtime(self):
        return time.time()
    
    ########### Private functions (git -> svn mapping) ###########
    
    def _commit_by_revision(self, revision):
        return self.git_repo.commit(self.repo_lineariser[revision])
    
    
    def _tree_for_commit(self, commit, name, parent, rev):
        
        tree = self.git_repo.tree(commit.tree)
        
        root = Directory(name=name, parent=parent, rev=rev)
        
        # Loop through the files, and make a tree.
        dirs = {"": root}
        for mode, path, shaid in tree.entries():
            # Make sure we have directories for our full path.
            prev_path = ""
            for part in path.split("/")[:-1]:
                # If this directory isn't made yet...
                our_path = prev_path + "/" + part
                if our_path not in dirs:
                    # Make it.
                    dirs[our_path] = Directory(
                        name = part, 
                        parent = dirs[prev_path],
                        rev = rev,
                    )
                prev_path = our_path
            
            File(
                name = path.split("/")[-1],
                contents = self.git_repo.get_object(shaid).as_pretty_string(),
                parent = dirs[prev_path],
                rev = rev,
            )
        # Return!
        return root
    

class RepositoryLineariser(object):
    
    """
    Because git doesn't helpfully assign local-only linear revision numbers
    (better DVCSen do *cough*), we have to do this manually.
    This class represents a mapping from hashes -> revision numbers, and is
    backed by disk.
    """

    def __init__(self, repo):
        self.repo = repo
        self.map_path = os.path.join(repo.directory, ".heechee_revs")
        self.load()
    
    def load(self):
        "Loads the revision map from the file."
        self.revmap = {}
        config = RawConfigParser()
        config.read(self.map_path)
        if config.has_section("revmap"):
            for option in config.options("revmap"):
                self.revmap[int(option)] = config.get("revmap", option)
    
    def save(self):
        "Saves the revision map to the file."
        config = RawConfigParser()
        config.add_section("revmap")
        for revnum, hash in sorted(self.revmap.items()):
            config.set("revmap", str(revnum), hash)
        fp = open(self.map_path, "w")
        config.write(fp)
        fp.close()
    
    def __getitem__(self, key):
        return self.revmap[key]
    
    def top_revision_number(self):
        "Returns the highest revision number in the repository"
        try:
            return max(self.revmap.keys())
        except ValueError:
            return 0
    
    def add_missing(self, head):
        """
        Takes a head revision, goes through all its ancestors, and makes sure
        there is a linear revision number for all of them.
        """
        for commit in reversed(self.repo.git_repo.revision_history(head)):
            if commit.id not in self.revmap.values():
                new_revision = self.top_revision_number() + 1
                self.revmap[new_revision] = commit.id
        self.save()