EMPTY_TREE = b'4b825dc642cb6eb9a060e54bf8d69288fbee4904' class Git(object):
    _notes_depth = {}
    _config = None
    _replace = {}

    @classmethod
    def iter(self, *args, **kwargs):
        start = time.time()

        if args[0] == 'config':
            self._config = None

        proc = GitProcess(*args, **kwargs)
        try:
            for line in proc.stdout or ():
                line = line.rstrip(b'\n')
                yield line

        finally:
            proc.wait()
            logging.getLogger(args[0]).info('[%d] wall time: %.3fs',
                                            proc.pid, time.time() - start)

    @classmethod
    def run(self, *args, **kwargs):
        stdout = kwargs.pop('stdout', None)
        return tuple(self.iter(*args, stdout=stdout, **kwargs))

    @classmethod
    def for_each_ref(self, *patterns):
        if not patterns:
            return
        # Ideally, this would not actually call for-each-ref if all refs
        # matching the given patterns are already known.
        for line in self.iter('for-each-ref', '--format',
                              '%(objectname) %(refname)', *patterns,
                              stderr=open(os.devnull, 'wb')):
            yield line.split(b' ', 1)

    @classmethod
    def resolve_ref(self, ref):
        return one(Git.iter('rev-parse', '--revs-only', ref,
                            stderr=open(os.devnull, 'wb')))

    @classmethod
    def ls_tree(self, treeish, path=b'', recursive=False):
        from .helper import GitHgHelper
        assert not treeish.startswith(b'refs/')

        if path.endswith(b'/') or recursive or path == b'':
            path = path.rstrip(b'/')
            for line in GitHgHelper.ls_tree(b'%s:%s' % (treeish, path),
                                            recursive):
                mode, typ, sha1, p = line
                if path:
                    yield mode, typ, sha1, posixpath.join(path, p)
                else:
                    yield mode, typ, sha1, p
        else:
            with GitHgHelper.query(b'ls', treeish, path) as stdout:
                line = stdout.readline()
                if not line.startswith(b'missing '):
                    yield split_ls_tree(line[:-1])

    @classmethod
    def update_ref(self, ref, newvalue):
        assert not newvalue.startswith(b'refs/')
        from .helper import GitHgHelper
        GitHgHelper.update_ref(ref, newvalue)

    @classmethod
    def delete_ref(self, ref):
        self.update_ref(ref, NULL_NODE_ID)

    @classmethod
    def config(self, name, remote=None, values={}, multiple=False):
        assert not (values and multiple)
        if self._config is None:
            proc = GitProcess('config', '-l', '-z')
            data = proc.stdout.read()
            proc.wait()
            self._config = {}
            for l in data.split(b'\0'):
                if l:
                    k, v = l.split(b'\n', 1)
                    if k in self._config:
                        self._config[k] += b'\0' + v
                    else:
                        self._config[k] = v
        var = name.encode('ascii')
        value = None
        if name.startswith('cinnabar.'):
            var = ('GIT_%s' % name.replace('.', '_').upper()).encode('ascii')
            value = environ(var)
            if value is None and remote:
                var = b'remote.%s.%s' % (
                    remote, name.replace('.', '-').encode('ascii'))
                value = self._config.get(var.lower())
        elif name == 'fetch.prune' and remote:
            var = b'remote.%s.prune' % remote
            value = self._config.get(var.lower())
        if value is None:
            var = name.encode('ascii')
            value = self._config.get(var.lower())
        if value:
            value = value.split(b'\0')
            if not multiple:
                value = value[-1]
        logging.getLogger('config').info('%s = %r', var, value)
        if values:
            if value in values:
                if isinstance(values, dict):
                    value = values[value]
            else:
                values = ', '.join(sorted('"%s"' % v.decode('ascii')
                                          for v in values
                                          if v is not None))
                if value is None:
                    raise InvalidConfig(
                        '%s must be set to one of %s' % (
                            var.decode('ascii'), values))
                else:
                    raise InvalidConfig(
                        'Invalid value for %s: "%s". Valid values: %s' % (
                            var.decode('ascii'), fsdecode(value), values))
        return value NULL_NODE_ID = b'0' * 40 class GitCommit(object):
    __slots__ = ('sha1', 'body', 'parents', 'tree', 'author', 'committer')

    def __init__(self, sha1):
        self.sha1 = sha1
        commit = GitHgHelper.cat_file(b'commit', sha1)
        header, self.body = commit.split(b'\n\n', 1)
        parents = []
        for line in header.splitlines():
            if line == b'\n':
                break
            typ, data = line.split(b' ', 1)
            typ = typ.decode('ascii')
            if typ == 'parent':
                parents.append(data.strip())
            elif typ in self.__slots__:
                assert not hasattr(self, typ)
                setattr(self, typ, data)
        self.parents = tuple(parents) class GitHgStore(object):
    FLAGS = [
        b'files-meta',
        b'unified-manifests-v2',
    ]

    METADATA_REFS = (
        b'refs/cinnabar/changesets',
        b'refs/cinnabar/manifests',
        b'refs/cinnabar/hg2git',
        b'refs/notes/cinnabar',
        b'refs/cinnabar/files-meta',
    )

    def _metadata(self):
        if self._metadata_sha1:
            metadata = GitCommit(self._metadata_sha1)
            self._flags = set(metadata.body.split())
            refs = self.METADATA_REFS
            if b'files-meta' not in self._flags:
                refs = list(refs)
                refs.remove(b'refs/cinnabar/files-meta')
            return metadata, dict(zip(refs, metadata.parents))

    def metadata(self):
        metadata = self._metadata()
        if metadata:
            if len(self._flags) > len(self.FLAGS):
                raise UpgradeAbort(
                    'It looks like this repository was used with a newer '
                    'version of git-cinnabar. Cannot use this version.')
            if set(self._flags) != set(self.FLAGS):
                raise UpgradeAbort()
        return metadata

    def __init__(self):
        self._flags = set()
        self._closed = False
        self._graft = None

        self._hgheads = VersionedDict()
        self._branches = {}

        self._replace = Git._replace
        self._tagcache_ref = None
        self._metadata_sha1 = None
        # While doing a for_each_ref, ensure refs/notes/cinnabar is in the
        # cache.
        for sha1, ref in Git.for_each_ref('refs/cinnabar',
                                          'refs/notes/cinnabar'):
            if ref.startswith(b'refs/cinnabar/replace/'):
                self._replace[ref[22:]] = sha1
            elif ref.startswith(b'refs/cinnabar/branches/'):
                raise OldUpgradeAbort()
            elif ref == b'refs/cinnabar/metadata':
                self._metadata_sha1 = sha1
            elif ref == b'refs/cinnabar/tag_cache':
                self._tagcache_ref = sha1
        self._replace = VersionedDict(self._replace)

        self._tagcache = {}
        self._tagfiles = {}
        self._tags = {NULL_NODE_ID: {}}
        self._cached_changeset_ref = {}
        self._tagcache_items = set()
        if self._tagcache_ref:
            for line in Git.ls_tree(self._tagcache_ref):
                mode, typ, sha1, path = line
                if typ == b'blob':
                    if self.ATTR[mode] == b'x':
                        self._tagfiles[path] = sha1
                    else:
                        self._tagcache[path] = sha1
                elif typ == b'commit':
                    assert sha1 == NULL_NODE_ID
                    self._tagcache[path] = sha1
                self._tagcache_items.add(path)

        self.tag_changes = False

        metadata = self.metadata()
        if metadata:
            metadata, refs = metadata
        self._has_metadata = bool(metadata)
        self._metadata_refs = refs if metadata else {}
        self._manifest_heads_orig = set()
        self._generation = 0
        if metadata:
            changesets_ref = self._metadata_refs.get(
                b'refs/cinnabar/changesets')
            if changesets_ref:
                commit = GitCommit(changesets_ref)
                for n, head in enumerate(commit.body.splitlines()):
                    hghead, branch = head.split(b' ', 1)
                    self._hgheads._previous[hghead] = (branch, n)
                    self._generation = n + 1

            self._manifest_heads_orig = set(GitHgHelper.heads(b'manifests'))

            replace = {}
            for line in Git.ls_tree(metadata.tree):
                mode, typ, sha1, path = line
                replace[path] = sha1

            if self._replace and not replace:
                raise OldUpgradeAbort()

            # Delete old tag-cache, which may contain incomplete data.
            Git.delete_ref(b'refs/cinnabar/tag-cache')

    def prepare_graft(self):
        self._graft = Grafter(self)

    @staticmethod
    def _try_merge_branches(repo_url):
        parsed_url = urlparse(repo_url)
        branches = []
        path = parsed_url.path.lstrip(b'/').rstrip(b'/')
        if path:
            parts = list(reversed(path.split(b'/')))
        else:
            parts = []
        host = parsed_url.netloc.split(b':', 1)[0]
        if host:
            parts.append(host)
        last_path = b''
        for part in parts:
            if last_path:
                last_path = b'%s/%s' % (part, last_path)
            else:
                last_path = part
            branches.append(last_path)
        branches.append(b'metadata')
        return branches

    @staticmethod
    def _find_branch(branches, remote_refs):
        for branch in branches:
            if branch in remote_refs:
                return branch
            if b'refs/cinnabar/%s' % branch in remote_refs:
                return b'refs/cinnabar/%s' % branch
            if b'refs/heads/%s' % branch in remote_refs:
                return b'refs/heads/%s' % branch

    def merge(self, git_repo_url, hg_repo_url, branch=None):
        # Eventually we'll want to handle a full merge, but for now, we only
        # handle the case where we don't have metadata to begin with.
        # The caller should avoid calling this function otherwise.
        assert not self._has_metadata
        remote_refs = OrderedDict()
        for line in Git.iter('ls-remote', fsdecode(git_repo_url),
                             stderr=open(os.devnull, 'wb')):
            sha1, ref = line.split(None, 1)
            remote_refs[ref] = sha1
        bundle = None
        if not remote_refs and urlparse(git_repo_url).scheme in (b'http',
                                                                 b'https'):
            try:
                bundle = HTTPReader(git_repo_url)
            except URLError as e:
                logging.error(e.reason)
                return False
            if bundle.fh.headers.get('Content-Encoding', 'identity') == 'gzip':
                from gzip import GzipFile
                bundle = Seekable(bundle, bundle.length)
                bundle = GzipFile(mode='rb', fileobj=bundle)
            BUNDLE_SIGNATURE = b'# v2 git bundle\n'
            signature = bundle.read(len(BUNDLE_SIGNATURE))
            if signature != BUNDLE_SIGNATURE:
                logging.error('Could not find cinnabar metadata')
                return False
            bundle = io.BufferedReader(bundle)
            while True:
                line = bundle.readline().rstrip()
                if not line:
                    break
                sha1, ref = line.split(b' ', 1)
                remote_refs[ref] = sha1
        if branch:
            branches = [branch]
        else:
            branches = self._try_merge_branches(hg_repo_url)

        ref = self._find_branch(branches, remote_refs)
        if ref is None:
            logging.error('Could not find cinnabar metadata')
            return False

        if bundle:
            args = ('-v',) if util.progress else ()
            proc = GitProcess('index-pack', '--stdin', '--fix-thin', *args,
                              stdin=subprocess.PIPE,
                              stdout=open(os.devnull, 'wb'))
            shutil.copyfileobj(bundle, proc.stdin)
        else:
            fetch = ['fetch', '--no-tags', '--no-recurse-submodules', '-q']
            fetch.append('--progress' if util.progress else '--no-progress')
            fetch.append(fsdecode(git_repo_url))
            cmd = fetch + [fsdecode(ref) + ':refs/cinnabar/fetch']
            proc = GitProcess(*cmd, stdout=sys.stdout)
        if proc.wait():
            logging.error('Failed to fetch cinnabar metadata.')
            return False

        # Do some basic validation on the metadata we just got.
        commit = GitCommit(remote_refs[ref])
        if b'cinnabar@git' not in commit.author:
            logging.error('Invalid cinnabar metadata.')
            return False

        flags = set(commit.body.split())
        if b'files-meta' not in flags or b'unified-manifests-v2' not in flags \
                or len(commit.parents) != len(self.METADATA_REFS):
            logging.error('Invalid cinnabar metadata.')
            return False

        # At this point, we'll just assume this is good enough.

        # Get replace refs.
        if commit.tree != EMPTY_TREE:
            errors = False
            by_sha1 = {}
            for k, v in util.iteritems(remote_refs):
                if v not in by_sha1:
                    by_sha1[v] = k
            needed = []
            for line in Git.ls_tree(commit.tree):
                mode, typ, sha1, path = line
                if sha1 in by_sha1:
                    ref = b'refs/cinnabar/replace/%s' % path
                    if bundle:
                        Git.update_ref(ref, sha1)
                    else:
                        needed.append(
                            fsdecode(b':'.join((by_sha1[sha1], ref))))
                else:
                    logging.error('Missing commit: %s', sha1)
                    errors = True
            if errors:
                return False

            if not bundle:
                cmd = fetch + needed
                proc = GitProcess(*cmd, stdout=sys.stdout)
                if proc.wait():
                    logging.error('Failed to fetch cinnabar metadata.')
                    return False

        Git.update_ref(b'refs/cinnabar/metadata', commit.sha1)
        self._metadata_sha1 = commit.sha1
        GitHgHelper.reload()
        Git.delete_ref(b'refs/cinnabar/fetch')

        # TODO: avoid the duplication of code with __init__
        metadata = self.metadata()

        if not metadata:
            # This should never happen, but just in case.
            logging.warn('Could not find cinnabar metadata')
            Git.delete_ref(b'refs/cinnabar/metadata')
            GitHgHelper.reload()
            return False

        metadata, refs = metadata
        self._has_metadata = True
        self._metadata_refs = refs if metadata else {}
        changesets_ref = self._metadata_refs.get(b'refs/cinnabar/changesets')
        self._generation = 0
        if changesets_ref:
            commit = GitCommit(changesets_ref)
            for n, head in enumerate(commit.body.splitlines()):
                hghead, branch = head.split(b' ', 1)
                self._hgheads._previous[hghead] = (branch, 1)
                self._generation = n + 1

        self._manifest_heads_orig = set(GitHgHelper.heads(b'manifests'))

        for line in Git.ls_tree(metadata.tree):
            mode, typ, sha1, path = line
            self._replace[path] = sha1

        return True

    def tags(self):
        tags = TagSet()
        heads = sorted((n, h) for h, (b, n) in util.iteritems(self._hgheads))
        for _, h in heads:
            h = self.changeset_ref(h)
            tags.update(self._get_hgtags(h))
        for tag, node in tags:
            if node != NULL_NODE_ID:
                yield tag, node

    def _get_hgtags(self, head):
        tags = TagSet()
        if not self._tagcache.get(head):
            ls = one(Git.ls_tree(head, b'.hgtags'))
            if not ls:
                self._tagcache[head] = NULL_NODE_ID
                return tags
            mode, typ, self._tagcache[head], path = ls
        tagfile = self._tagcache[head]
        if tagfile not in self._tags:
            if tagfile in self._tagfiles:
                data = GitHgHelper.cat_file(b'blob', self._tagfiles[tagfile])
                for line in data.splitlines():
                    tag, nodes = line.split(b'\0', 1)
                    nodes = nodes.split(b' ')
                    for node in reversed(nodes):
                        tags[tag] = node
            else:
                data = GitHgHelper.cat_file(b'blob', tagfile) or b''
                for line in data.splitlines():
                    if not line:
                        continue
                    try:
                        node, tag = line.split(b' ', 1)
                    except ValueError:
                        continue
                    tag = tag.strip()
                    try:
                        unhexlify(node)
                    except TypeError:
                        continue
                    if node != NULL_NODE_ID:
                        node = self.cached_changeset_ref(node)
                    if node:
                        tags[tag] = node
            self._tags[tagfile] = tags
        return self._tags[tagfile]

    def heads(self, branches={}):
        if not isinstance(branches, (dict, set)):
            branches = set(branches)
        return set(h for h, (b, _) in util.iteritems(self._hgheads)
                   if not branches or b in branches)

    def _head_branch(self, head):
        if head in self._hgheads:
            return self._hgheads[head][0], head
        if head in self._branches:
            return self._branches[head], head
        branch = self.changeset(head).branch or b'default'
        self._branches[head] = branch
        return branch, head

    def add_head(self, head, parent1=NULL_NODE_ID, parent2=NULL_NODE_ID):
        branch, head = self._head_branch(head)
        for p in (parent1, parent2):
            if p == NULL_NODE_ID:
                continue
            parent_branch, parent_head = self._head_branch(p)
            if parent_branch == branch:
                if parent_head in self._hgheads:
                    assert parent_branch == self._hgheads[parent_head][0]
                    del self._hgheads[parent_head]

        generation = self._generation
        self._generation += 1
        self._hgheads[head] = (branch, generation)

    def read_changeset_data(self, obj):
        assert obj is not None
        obj = bytes(obj)
        data = GitHgHelper.git2hg(obj)
        if data is None:
            return None
        ret = ChangesetPatcher(data)
        return ret

    def hg_changeset(self, sha1):
        data = self.read_changeset_data(sha1)
        if data:
            assert data.startswith(b'changeset ')
            return data[10:50]
        return None

    def hg_manifest(self, sha1):
        git_commit = GitCommit(sha1)
        assert len(git_commit.body) == 40
        return git_commit.body

    def _hg2git(self, sha1):
        if not self._has_metadata and not GitHgHelper._helper:
            return None
        gitsha1 = GitHgHelper.hg2git(sha1)
        if gitsha1 == NULL_NODE_ID:
            gitsha1 = None
        return gitsha1

    def changeset(self, sha1, include_parents=False):
        gitsha1 = self.changeset_ref(sha1)
        assert gitsha1
        return self._changeset(gitsha1, include_parents)

    def _changeset(self, git_commit, include_parents=False):
        if not isinstance(git_commit, GitCommit):
            git_commit = GitCommit(git_commit)

        metadata = self.read_changeset_data(git_commit.sha1)
        if not metadata:
            return None
        changeset = Changeset.from_git_commit(git_commit)
        changeset = metadata.apply(changeset)

        if include_parents:
            assert len(git_commit.parents) <= 2
            changeset.parents = tuple(
                self.hg_changeset(self._replace.get(p, p))
                for p in git_commit.parents)

        return changeset

    ATTR = {
        b'100644': b'',
        b'100755': b'x',
        b'120000': b'l',
    }

    @staticmethod
    def manifest_metadata_path(path):
        return b'_' + path.replace(b'/', b'/_')

    @staticmethod
    def manifest_path(path):
        return path[1:].replace(b'/_', b'/')

    def manifest(self, sha1, include_parents=False):
        manifest = GeneratedManifestInfo(sha1)
        manifest.raw_data = GitHgHelper.manifest(sha1)
        if include_parents:
            git_sha1 = self.manifest_ref(sha1)
            commit = GitCommit(git_sha1)
            parents = (self.hg_manifest(p) for p in commit.parents)
            manifest.parents = tuple(parents)
        return manifest

    def manifest_ref(self, sha1):
        return self._hg2git(sha1)

    def changeset_ref(self, sha1):
        return self._hg2git(sha1)

    def cached_changeset_ref(self, sha1):
        try:
            return self._cached_changeset_ref[sha1]
        except KeyError:
            res = self._cached_changeset_ref[sha1] = self.changeset_ref(sha1)
            return res

    def file_meta(self, sha1):
        return GitHgHelper.file_meta(sha1)

    def file(self, sha1, file_parents=None):
        if sha1 == HG_EMPTY_FILE:
            content = b''
        else:
            content = GitHgHelper.cat_blob(b':h%s' % sha1)

        file = File(sha1)
        meta = self.file_meta(sha1)
        if meta:
            file.metadata = meta
        file.content = content
        if file_parents is not None:
            FileFindParents.set_parents(file, *file_parents)
        return file

    def git_file_ref(self, sha1):
        # Because an empty file and an empty manifest, both with no parents,
        # have the same sha1, we can't store both in the hg2git tree. So, we
        # choose to never store the file version, and make it forcibly resolve
        # to the empty blob. Which means we won't be storing an empty blob and
        # getting a mark for it, and will attempt to use it directly even if
        # it doesn't exist. The FastImport code works around this.
        # Theoretically, it is possible to have a non-modified child of the
        # empty file, and a non-modified child of the empty manifest, which
        # both would also have the same sha1, but, TTBOMK, it is only possible
        # to achieve with commands like hg debugparents.
        if sha1 == HG_EMPTY_FILE:
            return EMPTY_BLOB
        return self._hg2git(sha1)

    def git_tree(self, manifest_sha1, ref_changeset=None):
        if manifest_sha1 == NULL_NODE_ID:
            return EMPTY_TREE
        return GitHgHelper.create_git_tree(manifest_sha1, ref_changeset)

    def store_changeset(self, instance, commit=None):
        if commit and not isinstance(commit, GitCommit):
            commit = GitCommit(commit)
        if commit is None and self._graft:
            return self._graft.graft(instance)

        if not commit:
            author = Authorship.from_hg(instance.author, instance.timestamp,
                                        instance.utcoffset)
            extra = instance.extra
            if extra and extra.get(b'committer'):
                committer = extra[b'committer']
                if committer[-1:] == b'>':
                    committer = Authorship.from_hg(
                        committer, instance.timestamp, instance.utcoffset)
                else:
                    committer = Authorship.from_hg_str(
                        committer, maybe_git_utcoffset=True)
                    if committer.to_hg() == committer:
                        extra = dict(instance.extra)
                        del extra[b'committer']
                        if not extra:
                            extra = None
            else:
                committer = author

            parents = tuple(b':h%s' % p for p in instance.parents)

            body = instance.body

            # There are cases where two changesets would map to the same
            # git commit because their differences are not in information
            # stored in the git commit (different manifest node, but
            # identical tree ; different branches ; etc.)
            # In that case, add invisible characters to the commit
            # message until we find a commit that doesn't map to another
            # changeset.
            committer = committer.to_git_str()
            author = author.to_git_str()
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/tip',
                message=body,
                committer=committer,
                author=author,
                parents=parents,
                pseudo_mark=b':h%s' % instance.node,
            ) as c:
                c.filemodify(b'', self.git_tree(instance.manifest,
                                                *instance.parents[:1]),
                             typ=b'tree')

            commit = PseudoGitCommit(b':1')
            commit.author = author
            commit.committer = committer
            commit.body = body

        GitHgHelper.set(b'changeset', instance.node, commit.sha1)
        changeset = Changeset.from_git_commit(commit)
        GitHgHelper.put_blob(
            ChangesetPatcher.from_diff(changeset, instance), want_sha1=False)
        GitHgHelper.set(b'changeset-metadata', instance.node, b':1')

        self._branches[instance.node] = instance.branch or b'default'
        self.add_head(instance.node, instance.parent1, instance.parent2)

    MODE = {
        b'': b'160644',
        b'l': b'160000',
        b'x': b'160755',
    }

    def store_manifest(self, instance):
        if getattr(instance, 'delta_node', NULL_NODE_ID) != NULL_NODE_ID:
            previous = b':h%s' % instance.delta_node
        else:
            previous = None
        parents = tuple(b':h%s' % p for p in instance.parents)
        with GitHgHelper.commit(
            ref=b'refs/cinnabar/manifests',
            from_commit=previous,
            parents=parents,
            message=instance.node,
            pseudo_mark=b':h%s' % instance.node,
        ) as commit:
            if hasattr(instance, 'delta_node'):
                for name in instance.removed:
                    commit.filedelete(self.manifest_metadata_path(name))
                modified = instance.modified.items()
            else:
                # slow
                modified = ((line.path, (line.sha1, line.attr))
                            for line in instance)
            for name, (node, attr) in modified:
                node = bytes(node)
                commit.filemodify(self.manifest_metadata_path(name), node,
                                  self.MODE[attr])

        GitHgHelper.set(b'manifest', instance.node, b':1')

        if check_enabled('manifests'):
            if not GitHgHelper.check_manifest(instance.node):
                raise Exception(
                    'sha1 mismatch for node %s with parents %s %s and '
                    'previous %s' %
                    (instance.node.decode('ascii'),
                     instance.parent1.decode('ascii'),
                     instance.parent2.decode('ascii'),
                     instance.delta_node.decode('ascii'))
                )

    def close(self, refresh=()):
        if self._closed:
            return
        if self._graft:
            self._graft.close()
        self._closed = True
        # If the helper is not running, we don't have anything to update.
        if not GitHgHelper._helper:
            return
        update_metadata = {}
        tree = GitHgHelper.store(b'metadata', b'hg2git')
        if tree != NULL_NODE_ID:
            hg2git = self._metadata_refs.get(b'refs/cinnabar/hg2git')
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/hg2git',
            ) as commit:
                commit.write(b'M 040000 %s \n' % tree)
            if commit.sha1 != hg2git:
                update_metadata[b'refs/cinnabar/hg2git'] = commit.sha1

        tree = GitHgHelper.store(b'metadata', b'git2hg')
        if tree != NULL_NODE_ID:
            notes = self._metadata_refs.get(b'refs/notes/cinnabar')
            with GitHgHelper.commit(
                ref=b'refs/notes/cinnabar',
            ) as commit:
                commit.write(b'M 040000 %s \n' % tree)
            if commit.sha1 != notes:
                update_metadata[b'refs/notes/cinnabar'] = commit.sha1

        hg_changeset_heads = list(self._hgheads)
        changeset_heads = list(self.changeset_ref(h)
                               for h in hg_changeset_heads)
        if (any(self._hgheads.iterchanges()) or
                b'refs/cinnabar/changesets' in refresh):
            heads = sorted((self._hgheads[h][1], self._hgheads[h][0], h, g)
                           for h, g in zip(hg_changeset_heads,
                                           changeset_heads))
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/changesets',
                parents=list(h for _, __, ___, h in heads),
                message=b'\n'.join(b'%s %s' % (h, b) for _, b, h, __ in heads),
            ) as commit:
                pass
            update_metadata[b'refs/cinnabar/changesets'] = commit.sha1

        changeset_heads = set(changeset_heads)

        manifest_heads = GitHgHelper.heads(b'manifests')
        if (set(manifest_heads) != self._manifest_heads_orig or
                (b'refs/cinnabar/changesets' in update_metadata and
                 not manifest_heads) or b'refs/cinnabar/manifests' in refresh):
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/manifests',
                parents=sorted(manifest_heads),
            ) as commit:
                pass
            update_metadata[b'refs/cinnabar/manifests'] = commit.sha1

        tree = GitHgHelper.store(b'metadata', b'files-meta')
        files_meta_ref = self._metadata_refs.get(b'refs/cinnabar/files-meta')
        if update_metadata and (tree != NULL_NODE_ID or not files_meta_ref):
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/files-meta',
            ) as commit:
                if tree != NULL_NODE_ID:
                    commit.write(b'M 040000 %s \n' % tree)
            if commit.sha1 != files_meta_ref:
                update_metadata[b'refs/cinnabar/files-meta'] = commit.sha1

        replace_changed = False
        for status, ref, sha1 in self._replace.iterchanges():
            if status == VersionedDict.REMOVED:
                Git.delete_ref(b'refs/cinnabar/replace/%s' % ref)
            else:
                Git.update_ref(b'refs/cinnabar/replace/%s' % ref, sha1)
            replace_changed = True

        if update_metadata or replace_changed:
            parents = list(update_metadata.get(r) or self._metadata_refs[r]
                           for r in self.METADATA_REFS)
            metadata_sha1 = (Git.config('cinnabar.previous-metadata') or
                             self._metadata_sha1)
            if metadata_sha1:
                parents.append(metadata_sha1)
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/metadata',
                parents=parents,
                message=b' '.join(sorted(self.FLAGS)),
            ) as commit:
                for sha1, target in util.iteritems(self._replace):
                    commit.filemodify(sha1, target, b'commit')

        for c in self._tagcache:
            if c not in changeset_heads:
                self._tagcache[c] = False

        for c in changeset_heads:
            if c not in self._tagcache:
                tags = self._get_hgtags(c)

        files = set(util.itervalues(self._tagcache))
        deleted = set()
        created = {}
        for f in self._tagcache_items:
            if (f not in self._tagcache and f not in self._tagfiles or
                    f not in files and f in self._tagfiles):
                deleted.add(f)

        def tagset_lines(tags):
            for tag, value in tags:
                yield b'%s\0%s %s\n' % (tag, value,
                                        b' '.join(tags.hist(tag)))

        for f, tags in util.iteritems(self._tags):
            if f not in self._tagfiles and f != NULL_NODE_ID:
                data = b''.join(tagset_lines(tags))
                mark = GitHgHelper.put_blob(data=data)
                created[f] = (mark, b'exec')

        if created or deleted:
            self.tag_changes = True

        for c, f in util.iteritems(self._tagcache):
            if (f and c not in self._tagcache_items):
                if f == NULL_NODE_ID:
                    created[c] = (f, b'commit')
                else:
                    created[c] = (f, b'regular')
            elif f is False and c in self._tagcache_items:
                deleted.add(c)

        if created or deleted:
            with GitHgHelper.commit(
                ref=b'refs/cinnabar/tag_cache',
                from_commit=self._tagcache_ref,
            ) as commit:
                for f in deleted:
                    commit.filedelete(f)

                for f, (filesha1, typ) in util.iteritems(created):
                    commit.filemodify(f, filesha1, typ)

        # refs/notes/cinnabar is kept for convenience
        for ref in update_metadata:
            if ref not in (b'refs/notes/cinnabar',):
                Git.delete_ref(ref)

        GitHgHelper.close(rollback=False)

        # Try to detect issue #207 as early as possible.
        GitHgHelper._helper = False
        busted = False
        from .hg.repo import getbundle_params, stored_files
        for (node, (parent1, parent2)) in progress_iter(
                "Checking {} imported file root and head revisions",
                util.iteritems(stored_files)):
            if not GitHgHelper.check_file(node, parent1, parent2):
                busted = True
                logging.error(
                    "Error in file %s" % node.decode('ascii', 'replace'))
        if busted:
            import json
            extra = bundle = ""
            if getbundle_params:
                extra = \
                    "If it failed, please also copy/paste the following:\n"
                extra += json.dumps(getbundle_params, sort_keys=True, indent=4)
            if check_enabled('unbundler') and "GIT_DIR" in os.environ:
                bundle = "Please keep a copy of the "
                bundle += os.environ["GIT_DIR"] + "/cinnabar-last-bundle file"
                bundle += " before doing the following.\n"
            raise Abort(
                "It seems you have hit a known, rare, and difficult to "
                "reproduce issue.\n"
                "Your help would be appreciated.\n" + bundle +
                "Please try either `git cinnabar rollback` followed by the "
                "same command that just\n"
                "failed, or `git cinnabar reclone`.\n"
                "Please open a new issue "
                "(https://github.com/glandium/git-cinnabar/issues/new)\n"
                "mentioning issue #207 and reporting whether the second "
                "attempt succeeded.\n" + extra + "\n"
                "Please read all the above and keep a copy of this repository."
            ) class RawRevChunk01(RawRevChunk):
    __slots__ = ('__weakref__',)

    node = RawRevChunk._field(0, 20, hexlify)
    parent1 = RawRevChunk._field(20, 20, hexlify)
    parent2 = RawRevChunk._field(40, 20, hexlify)
    changeset = RawRevChunk._field(60, 20, hexlify)
    data = RawRevChunk._field(80)
    patch = RawRevChunk._field(80, filter=RevDiff)

    # Because we keep so many instances of this class on hold, the overhead
    # of having a __dict__ per instance is a deal breaker.
    _delta_nodes = {}

    @property
    def delta_node(self):
        return self._delta_nodes.get(self.node, NULL_NODE_ID)

    @delta_node.setter
    def delta_node(self, value):
        self._delta_nodes[self.node] = value class RawRevChunk02(RawRevChunk):
    __slots__ = ()

    node = RawRevChunk._field(0, 20, hexlify)
    parent1 = RawRevChunk._field(20, 20, hexlify)
    parent2 = RawRevChunk._field(40, 20, hexlify)
    delta_node = RawRevChunk._field(60, 20, hexlify)
    changeset = RawRevChunk._field(80, 20, hexlify)
    data = RawRevChunk._field(100)
    patch = RawRevChunk._field(100, filter=RevDiff) class Changeset(HgObject):
    __slots__ = ('manifest', 'author', 'timestamp', 'utcoffset', 'body',
                 '__weakref__')

    def __init__(self, *args, **kwargs):
        super(Changeset, self).__init__(*args, **kwargs)
        self.manifest = NULL_NODE_ID
        self.author = b''
        self.timestamp = b''
        self.utcoffset = b''
        self.files = []
        self.body = b''

    @classmethod
    def from_chunk(cls, raw_chunk, delta_cs=None):
        this = super(Changeset, cls).from_chunk(raw_chunk, delta_cs)
        this.raw_data = raw_chunk.patch.apply(
            delta_cs.raw_data if delta_cs else b'')
        return this

    @HgObject.raw_data.setter
    def raw_data(self, data):
        metadata, self.body = data.split(b'\n\n', 1)
        lines = metadata.splitlines()
        self.manifest, self.author, date = lines[:3]
        date = date.split(b' ', 2)
        self.timestamp = date[0]
        self.utcoffset = date[1]
        if len(date) == 3:
            self.extra = date[2]
        self.files = lines[3:]

    files = TypedProperty(list)

    class ExtraData(dict):
        @classmethod
        def from_str(cls, s):
            return cls(i.split(b':', 1) for i in s.split(b'\0') if i)

        @classmethod
        def from_obj(cls, obj):
            if obj is None:
                return None
            if isinstance(obj, dict):
                return cls(obj)
            return cls.from_str(obj)

        def __str__(self):
            raise RuntimeError('Use to_str()')

        def to_str(self):
            return b'\0'.join(b':'.join(i) for i in sorted(self.items()))

    extra = TypedProperty(ExtraData)

    def _data_iter(self):
        yield self.manifest
        yield b'\n'
        yield self.author
        yield b'\n'
        yield self.timestamp
        yield b' '
        yield self.utcoffset
        if self.extra is not None:
            yield b' '
            yield self.extra.to_str()
        if self.files:
            yield b'\n'
            yield b'\n'.join(sorted(self.files))
        yield b'\n\n'
        yield self.body

    @property
    def changeset(self):
        return self.node

    @changeset.setter
    def changeset(self, value):
        assert value in (self.node, NULL_NODE_ID)

    class ExtraProperty(object):
        def __init__(self, name):
            self._name = name.encode('ascii')

        def __get__(self, obj, type=None):
            if obj.extra is None:
                return None
            return obj.extra.get(self._name)

        def __set__(self, obj, value):
            if not value:
                if obj.extra:
                    try:
                        del obj.extra[self._name]
                    except KeyError:
                        pass
                if not obj.extra:
                    obj.extra = None
            else:
                if obj.extra is None:
                    obj.extra = {}
                obj.extra[self._name] = value

    branch = ExtraProperty('branch')
    committer = ExtraProperty('committer')
    close = ExtraProperty('close') class File(HgObject):
    __slots__ = ('content', '__weakref__')

    def __init__(self, *args, **kwargs):
        super(File, self).__init__(*args, **kwargs)
        self.content = b''
        self.metadata = {}

    @classmethod
    def from_chunk(cls, raw_chunk, delta_file=None):
        this = super(File, cls).from_chunk(raw_chunk, delta_file)
        this.raw_data = raw_chunk.patch.apply(
            delta_file.raw_data if delta_file else b'')
        return this

    @HgObject.raw_data.setter
    def raw_data(self, data):
        if data.startswith(b'\1\n'):
            _, self.metadata, self.content = data.split(b'\1\n', 2)
        else:
            self.content = data

    class Metadata(OrderedDict):
        @classmethod
        def from_str(cls, s):
            return cls(
                l.split(b': ', 1)
                for l in s.splitlines()
            )

        @classmethod
        def from_dict(cls, d):
            if isinstance(d, OrderedDict):
                return cls(d)
            return cls(sorted(d.items()))

        @classmethod
        def from_obj(cls, obj):
            if isinstance(obj, dict):
                return cls.from_dict(obj)
            return cls.from_str(obj)

        def __str__(self):
            raise RuntimeError('Use to_str()')

        def to_str(self):
            return b''.join(b'%s: %s\n' % i for i in self.items())

    metadata = TypedProperty(Metadata)

    def _data_iter(self):
        metadata = self.metadata.to_str()
        if metadata or self.content.startswith(b'\1\n'):
            metadata = b'\1\n%s\1\n' % metadata
        if metadata:
            yield metadata
        if self.content:
            yield self.content class Manifest(HgObject):
    __slots__ = ('__weakref__', '_raw_data')

    def __init__(self, *args, **kwargs):
        super(Manifest, self).__init__(*args, **kwargs)
        self._items = []
        self._raw_data = None

    class ManifestItem(bytes):
        @classmethod
        def from_info(cls, path, sha1=None, attr=b''):
            if isinstance(path, cls):
                return path
            return cls(b'%s\0%s%s' % (path, sha1, attr))

        @property
        def path(self):
            attr_len = len(self.attr)
            assert self[-41 - attr_len:-40 - attr_len] == b'\0'
            return self[:-41 - attr_len]

        @property
        def attr(self):
            if self[-1] in b'lx':
                return self[-1:]
            return b''

        @property
        def sha1(self):
            attr_len = len(self.attr)
            if attr_len:
                return self[-40 - attr_len:-attr_len]
            return self[-40 - attr_len:]

    class ManifestList(list):
        def __init__(self, *args, **kwargs):
            super(Manifest.ManifestList, self).__init__(*args, **kwargs)
            self._last = None

        def append(self, value):
            assert isinstance(value, Manifest.ManifestItem)
            assert self._last is None or value > self._last
            super(Manifest.ManifestList, self).append(value)
            self._last = value

    _items = TypedProperty(ManifestList)

    @property
    def items(self):
        if self._raw_data is not None:
            self._items[:] = []
            for line in self._raw_data.splitlines():
                item = self.ManifestItem(line)
                self._items.append(item)
            self._raw_data = None
        return self._items

    def add(self, path, sha1=None, attr=b''):
        item = Manifest.ManifestItem.from_info(path, sha1, attr)
        self.items.append(item)

    def __iter__(self):
        return iter(self.items)

    def _data_iter(self):
        for item in self:
            yield item
            yield b'\n'

    @classmethod
    def from_chunk(cls, raw_chunk, delta_mn=None):
        this = super(Manifest, cls).from_chunk(raw_chunk, delta_mn)
        items = iter(delta_mn)
        offset = 0
        item = b''
        for part in raw_chunk.patch:
            while offset < part.start:
                item = next(items, None)
                if item is None:
                    break
                this._items.append(item)
                offset += len(item) + 1
            assert offset == part.start
            for item in part.text_data.tobytes().splitlines():
                item = this.ManifestItem(item)
                this._items.append(item)
            while offset < part.end:
                item = next(items, None)
                if item is None:
                    break
                offset += len(item) + 1
        for item in items:
            this._items.append(item)
        return this

    @property
    def raw_data(self):
        if self._raw_data is not None:
            return self._raw_data
        return super(Manifest, self).raw_data

    @raw_data.setter
    def raw_data(self, data):
        self._raw_data = bytes(data) class GitHgHelper(BaseHelper):
    VERSION = 3003
    MODE = 'import'
    _helper = False

    @classmethod
    def reload(self):
        with self.query(b'reload'):
            pass
        self.git2hg.invalidate()
        self.hg2git.invalidate()
        self._cat_commit.invalidate()
        # Force this method to be synchronous with the helper, using the
        # synchronous `version` command.
        with self.query(b'version %d' % self.VERSION) as stdout:
            stdout.readline()

    @classmethod
    def _cat_file(self, typ, sha1):
        with self.query(b'cat-file', sha1) as stdout:
            return self._read_file(typ, stdout)

    @classmethod
    @lrucache(16)
    def _cat_commit(self, sha1):
        return self._cat_file(b'commit', sha1)

    @classmethod
    def cat_file(self, typ, sha1):
        if typ == b'commit':
            return self._cat_commit(sha1)
        return self._cat_file(typ, sha1)

    @classmethod
    @lrucache(16)
    def git2hg(self, sha1):
        assert sha1 != b'changeset'
        with self.query(b'git2hg', sha1) as stdout:
            return self._read_file(b'blob', stdout)

    @classmethod
    def file_meta(self, sha1):
        with self.query(b'file-meta', sha1) as stdout:
            return self._read_file(b'blob', stdout)

    @classmethod
    @lrucache(16)
    def hg2git(self, hg_sha1):
        with self.query(b'hg2git', hg_sha1) as stdout:
            sha1 = stdout.read(41)
            assert sha1[-1:] == b'\n'
            return sha1[:40]

    @classmethod
    def manifest(self, hg_sha1):
        with self.query(b'manifest', hg_sha1) as stdout:
            return self._read_data(stdout)

    @classmethod
    def check_manifest(self, hg_sha1):
        with self.query(b'check-manifest', hg_sha1) as stdout:
            return stdout.readline().strip() == b'ok'

    @classmethod
    def check_file(self, hg_sha1, *parents):
        with self.query(b'check-file', hg_sha1, *parents) as stdout:
            return stdout.readline().strip() == b'ok'

    @classmethod
    def ls_tree(self, sha1, recursive=False):
        extra = () if not recursive else (b'-r',)
        with self.query(b'ls-tree', sha1, *extra) as stdout:
            for line in self._read_data(stdout).split(b'\0'):
                if line:
                    yield split_ls_tree(line)

    @classmethod
    def rev_list(self, *args):
        with self.query(b'rev-list', *args) as stdout:
            for line in self._read_data(stdout).splitlines():
                parents = line.split()
                commit = parents.pop(0)
                tree = parents.pop(0)
                yield commit, tree, parents

    @classmethod
    def diff_tree(self, rev1, rev2, detect_copy=False):
        extra = () if not detect_copy else (b'-C', b'-C')
        extra = extra + (b'--ignore-submodules=dirty', b'--')
        with self.query(b'diff-tree', rev1, rev2, *extra) as stdout:
            data = self._read_data(stdout)
            off = 0
            while off < len(data):
                tab = data.find(b'\t', off)
                assert tab != -1
                (mode_before, mode_after, sha1_before, sha1_after,
                 status) = data[off:tab].split(b' ')
                if detect_copy and status[:1] in b'RC':
                    orig = data.find(b'\0', tab + 1)
                    status = status[:1] + data[tab + 1:orig]
                    tab = orig
                end = data.find(b'\0', tab + 1)
                path = data[tab + 1:end]
                off = end + 1
                yield (mode_before, mode_after, sha1_before, sha1_after,
                       status, path)

    @classmethod
    def set(self, *args):
        if args[0] == b'changeset-metadata':
            self.git2hg.invalidate(self, self.hg2git(args[1]))
        elif args[0] != b'file-meta':
            self.hg2git.invalidate(self, args[1])
        with self.query(b'set', *args):
            pass

    @classmethod
    def store(self, what, *args):
        if what == b'metadata':
            with self.query(b'store', what, *args) as stdout:
                sha1 = stdout.read(41)
                assert sha1[-1:] == b'\n'
                return sha1[:40]
        elif what in (b'file', b'manifest'):
            obj = args[0]
            if isinstance(obj, RawRevChunk01):
                delta_node = obj.delta_node
            elif isinstance(obj, RawRevChunk02):
                delta_node = b'cg2'
            else:
                assert False
            with self.query(b'store', what, delta_node, b'%d' % len(obj)):
                self._helper.stdin.write(obj)
                self._helper.stdin.flush()
        elif what == b'manifest_changegroup':
            with self.query(b'store', what, *args):
                return self._helper.stdin
        else:
            assert False

    @classmethod
    @contextmanager
    def store_changegroup(self, version):
        with self.query(b'store', b'changegroup', b'%d' % version):
            yield self._helper.stdin

    @classmethod
    def heads(self, what):
        with self.query(b'heads', what) as stdout:
            data = self._read_data(stdout)
            return data.split()

    @classmethod
    def reset_heads(self, what):
        with self.query(b'reset-heads', what):
            pass

    @classmethod
    def upgrade(self):
        with self.query(b'upgrade') as stdout:
            return stdout.readline().strip() == b'ok'

    @classmethod
    def create_git_tree(self, manifest_sha1, ref_commit=None):
        extra_arg = (ref_commit,) if ref_commit else ()
        with self.query(b'create-git-tree', manifest_sha1,
                        *extra_arg) as stdout:
            sha1 = stdout.read(41)
            assert sha1[-1:] == b'\n'
            return sha1[:40]

    @classmethod
    def seen(self, typ, sha1):
        with self.query(b'seen', typ, sha1) as stdout:
            return stdout.readline().strip() == b'yes'

    @classmethod
    def dangling(self, typ):
        with self.query(b'dangling', typ) as stdout:
            data = self._read_data(stdout)
            return data.splitlines()

    @classmethod
    def update_ref(self, ref, newvalue):
        with self.query(b'reset', b'%s\nfrom %s\n' % (ref, newvalue)):
            self._helper.stdin.flush()

    @classmethod
    def cat_blob(self, ref):
        with self.query(b'cat-blob', ref) as stdout:
            sha1, blob, size = stdout.readline().split()
            assert blob == b'blob'
            size = int(size)
            content = stdout.read(size)
            lf = stdout.read(1)
            assert lf == b'\n'
            return content

    @classmethod
    def _get_last(self):
        with self.query(b'get-mark', b':1') as stdout:
            sha1 = stdout.read(41)
            assert sha1[-1:] == b'\n'
            return sha1[:40]

    @classmethod
    def put_blob(self, data=b'', want_sha1=True):
        with self.query(b'blob') as io:
            io.write(b'mark :1\n')
            io.write(b'data %d\n' % len(data))
            io.write(data)
            io.write(b'\n')
            io.flush()
            if want_sha1:
                return self._get_last()

    @classmethod
    @contextmanager
    def commit(self, ref, committer=b'<cinnabar@git> 0 +0000', author=None,
               message=b'', from_commit=None, parents=(), pseudo_mark=None):
        if isinstance(parents, GeneratorType):
            parents = tuple(parents)
        from_tree = None
        if parents and parents[0] == from_commit:
            _from = parents[0]
            merges = parents[1:]
        else:
            _from = NULL_NODE_ID
            merges = parents
            if from_commit:
                with self.query(b'ls', from_commit, b'') as stdout:
                    line = stdout.readline()
                    if line.startswith(b'missing '):
                        from_tree = None
                    else:
                        mode, typ, from_tree, path = split_ls_tree(line[:-1])

        helper = CommitHelper()
        helper.write(b'mark :1\n')
        # TODO: properly handle errors, like from the committer being badly
        # formatted.
        if author:
            helper.write(b'author %s\n' % author)
        helper.write(b'committer %s\n' % committer)
        helper.cmd_data(message)

        helper.write(b'from %s\n' % _from)
        for merge in merges:
            helper.write(b'merge %s\n' % merge)
        if from_tree:
            helper.write(b'M 040000 %s \n' % from_tree)

        yield helper
        helper.write(b'\n')

        with self.query(b'commit', ref) as stdout:
            stdout.write(helper.flush_buffer())
            stdout.flush()

        if not pseudo_mark:
            helper.sha1 = self._get_last()

    @classmethod
    def close(self, rollback=True):
        if not rollback and self._helper != self:
            with self.query(b'done'):
                pass
        super(GitHgHelper, self).close() def git_hash(type, data):
    h = hashlib.sha1(b'%s %d\0' % (type, len(data)))
    h.update(data)
    return h.hexdigest().encode('ascii') def one(iterable):
    lst = list(iterable)
    if lst:
        assert len(lst) == 1
        return lst[0]
    return None
import os
import shutil
import tempfile
import unittest
from binascii import unhexlify
from collections import OrderedDict
from itertools import chain
from cinnabar.git import (
    EMPTY_TREE,
    Git,
    NULL_NODE_ID,
)
from cinnabar.githg import (
    GitCommit,
    GitHgStore,
)
from cinnabar.hg.changegroup import (
    RawRevChunk01,
    RawRevChunk02,
)
from cinnabar.hg.objects import (
    Changeset,
    File,
    Manifest,
)
from cinnabar.helper import (
    GitHgHelper,
    git_hash,
)
from cinnabar.util import one
        GitHgHelper.store(b'manifest', chunk)

        store = GitHgStore()

        c = Changeset()
        c.manifest = m.node
        c.author = b'Cinnabar test <cinnabar@test>'
        c.timestamp = b'0'
        c.utcoffset = b'0'
        c.files = [i.path for i in m]
        c.body = b'Test commit'
        c.node = c.sha1

        store.store_changeset(c)
        c_gen = store.changeset(c.node)
        self.assertEqual(c.raw_data, c_gen.raw_data)

        commit = GitCommit(GitHgHelper.hg2git(c.node))
        self.assertEqual(commit.body, c.body)
        ct = self.commit_tree(m, files)
        self.assertEqual(commit.tree, ct)

        # Weird case as seen in the GNU octave repo.
        # The bar subdirectory is supposed to be transposed to the same
        # content as the git tree for the manifest above.
        m2 = Manifest()
        m2.add(b'bar/bar', f.node)
        m2.add(b'bar/foo/.foo', f.node)
        m2.add(b'bar/foo//.bar', f.node)
        m2.add(b'bar/foo//.foo', f2.node)
        m2.add(b'bar/foo//bar/baz', f2.node)
        m2.add(b'bar/foo//bar/foo', f.node)
        m2.add(b'bar/foo//hoge', f.node)
        m2.add(b'bar/foo/bar/baz', f.node)
        m2.add(b'bar/foo/bar/qux', f.node)
        m2.add(b'bar/foo/foo', f.node)
        m2.add(b'bar/foo/qux', f.node)
        m2.add(b'bar/qux', f.node)
        m2.node = m2.sha1

        chunk = m2.to_chunk(self.RevChunk, m)
        GitHgHelper.store(b'manifest', chunk)

        c2 = Changeset()
        c2.parent1 = c.node
        c2.manifest = m2.node
        c2.author = b'Cinnabar test <cinnabar@test>'
        c2.timestamp = b'0'
        c2.utcoffset = b'0'
        c2.files = [i.path for i in m2]
        c2.body = b'Test commit'
        c2.node = c2.sha1

        store.store_changeset(c2)
        c2_gen = store.changeset(c2.node)
        self.assertEqual(c2.raw_data, c2_gen.raw_data)

        commit = GitCommit(GitHgHelper.hg2git(c2.node))
        self.assertEqual(commit.body, c2.body)
        self.assertEqual(ct, one(Git.ls_tree(commit.tree, b'bar'))[2])

        # Corner case: empty manifest
        m3 = Manifest()
        m3.node = m3.sha1
        self.assertEqual(b'b80de5d138758541c5f05265ad144ab9fa86d1db', m3.node)

        chunk = m3.to_chunk(self.RevChunk, m2)
        GitHgHelper.store(b'manifest', chunk)
        commit = GitCommit(GitHgHelper.hg2git(m3.node))
        self.assertEqual(EMPTY_TREE, commit.tree)

        c3 = Changeset()
        c3.parent1 = c2.node
        c3.manifest = m3.node
        c3.author = b'Cinnabar test <cinnabar@test>'
        c3.timestamp = b'0'
        c3.utcoffset = b'0'
        c3.body = b'Test commit'
        c3.node = c3.sha1

        store.store_changeset(c3)
        c3_gen = store.changeset(c3.node)
        self.assertEqual(c3.raw_data, c3_gen.raw_data)

        commit = GitCommit(GitHgHelper.hg2git(c3.node))
        self.assertEqual(commit.body, c3.body)
        self.assertEqual(EMPTY_TREE, commit.tree)

        # Corner case: null manifest
        c4 = Changeset()
        c4.parent1 = c3.node
        c4.manifest = NULL_NODE_ID
        c4.author = b'Cinnabar test <cinnabar@test>'
        c4.timestamp = b'0'
        c4.utcoffset = b'0'
        c4.body = b'Test commit'
        c4.node = c4.sha1

        store.store_changeset(c4)
        c4_gen = store.changeset(c4.node)
        self.assertEqual(c4.raw_data, c4_gen.raw_data)

        commit = GitCommit(GitHgHelper.hg2git(c4.node))
        self.assertEqual(commit.body, c4.body)
        self.assertEqual(EMPTY_TREE, commit.tree)

        # Corner case: identical changeset with a difference that wouldn't
        # appear in the git commit without adjustment (which is: cinnabar adds
        # a nul character to the commit message..
        chunk = c2.to_chunk(RawRevChunk02)
        c5 = Changeset.from_chunk(chunk)
        c5.branch = b'branched'
        c5.node = c5.sha1

        store.store_changeset(c5)
        c5_gen = store.changeset(c5.node)
        self.assertEqual(c5.raw_data, c5_gen.raw_data)

        commit = GitCommit(GitHgHelper.hg2git(c5.node))
        self.assertEqual(commit.body, c5.body + b'\0')

        self.assertEqual(ct, one(Git.ls_tree(commit.tree, b'bar'))[2])