Exemple #1
0
 def __init__(self, store, remote_branchmap, remote_heads):
     self._heads = {}
     self._all_heads = tuple(autohexlify(h) for h in remote_heads)
     self._tips = {}
     self._git_sha1s = {}
     self._unknown_heads = set()
     for branch, heads in util.iteritems(remote_branchmap):
         # We can't keep track of tips if the list of heads is not sequenced
         sequenced = isinstance(heads, Sequence) or len(heads) == 1
         branch_heads = []
         for head in heads:
             head = autohexlify(head)
             branch_heads.append(head)
             sha1 = store.changeset_ref(head)
             if not sha1:
                 self._unknown_heads.add(head)
                 continue
             assert head not in self._git_sha1s
             self._git_sha1s[head] = sha1
         # Use last non-closed head as tip if there's more than one head.
         # Caveat: we don't know a head is closed until we've pulled it.
         if branch and heads and sequenced:
             for head in reversed(branch_heads):
                 self._tips[branch] = head
                 if head in self._git_sha1s:
                     changeset = store.changeset(head)
                     if changeset.close:
                         continue
                 break
         if branch:
             self._heads[branch] = tuple(branch_heads)
Exemple #2
0
 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
Exemple #3
0
 def update(self, other):
     if not other:
         return
     assert isinstance(other, TagSet)
     for key, anode in util.iteritems(other._tags):
         # derived from mercurial's _updatetags
         ahist = other._taghist[key]
         if key not in self._tags:
             self._tags[key] = anode
             self._taghist[key] = set(ahist)
             continue
         bnode = self._tags[key]
         bhist = self._taghist[key]
         if (bnode != anode and anode in bhist and
                 (bnode not in ahist or len(bhist) > len(ahist))):
             anode = bnode
         self._tags[key] = anode
         self._taghist[key] = ahist | set(
             n for n in bhist if n not in ahist)
Exemple #4
0
def bundle_data(store, commits):
    manifests = OrderedDict()
    files = defaultdict(list)

    for node, parents in progress_iter('Bundling {} changesets', commits):
        if len(parents) > 2:
            raise Exception(
                'Pushing octopus merges to mercurial is not supported')

        changeset_data = store.read_changeset_data(node)
        is_new = changeset_data is None or check_enabled('bundle')
        if is_new:
            store.create_hg_metadata(node, parents)
        hg_changeset = store._changeset(node, include_parents=True)
        if is_new:
            store.add_head(hg_changeset.node, hg_changeset.parent1,
                           hg_changeset.parent2)
        yield hg_changeset
        manifest = hg_changeset.manifest
        if manifest not in manifests and manifest != NULL_NODE_ID:
            if manifest not in (store.changeset(p).manifest
                                for p in hg_changeset.parents):
                manifests[manifest] = hg_changeset.node

    yield None

    for manifest, changeset in progress_iter('Bundling {} manifests',
                                             iteritems(manifests)):
        hg_manifest = store.manifest(manifest, include_parents=True)
        hg_manifest.changeset = changeset
        yield hg_manifest
        manifest_ref = store.manifest_ref(manifest)
        parents = tuple(store.manifest_ref(p) for p in hg_manifest.parents)
        changes = get_changes(manifest_ref, parents)
        for path, hg_file, hg_fileparents in changes:
            if hg_file != NULL_NODE_ID:
                files[store.manifest_path(path)].append(
                    (hg_file, hg_fileparents, changeset, parents))

    yield None

    def iter_files(files):
        count_chunks = 0
        for count_names, path in enumerate(sorted(files), 1):
            yield (count_chunks, count_names), path
            nodes = set()
            for node, parents, changeset, mn_parents in files[path]:
                if node in nodes:
                    continue
                count_chunks += 1
                nodes.add(node)
                file = store.file(node, parents, mn_parents, path)
                file.changeset = changeset
                assert file.node == file.sha1
                yield (count_chunks, count_names), file

            yield (count_chunks, count_names), None

    for chunk in progress_enum('Bundling {} revisions of {} files',
                               iter_files(files)):
        yield chunk

    yield None
Exemple #5
0
    def create_hg_manifest(self, commit, parents):
        manifest = GeneratedManifestInfo(NULL_NODE_ID)
        changeset_files = []

        if parents:
            parent_changeset = self.changeset(self.hg_changeset(parents[0]))
            parent_manifest = self.manifest(parent_changeset.manifest)
            parent_node = parent_manifest.node

        if len(parents) == 2:
            parent2_changeset = self.changeset(self.hg_changeset(parents[1]))
            parent2_manifest = self.manifest(parent2_changeset.manifest)
            parent2_node = parent2_manifest.node
            if parent_node == parent2_node:
                parents = parents[:1]

        if not parents:
            for line in Git.ls_tree(commit, recursive=True):
                mode, typ, sha1, path = line
                node = self.create_file(sha1,
                                        git_manifest_parents=(),
                                        path=path)
                manifest.add(path, node, self.ATTR[mode], modified=True)
                changeset_files.append(path)

            manifest.parents = []
            manifest.delta_node = NULL_NODE_ID
            return manifest, changeset_files

        elif len(parents) == 2:
            if not experiment('merge'):
                raise Exception('Pushing merges is not supported yet')
            if not self._merge_warn:
                logging.warning('Pushing merges is experimental.')
                logging.warning('This may irremediably push bad state to the '
                                'mercurial server!')
                self._merge_warn = 1
            git_manifests = (self.manifest_ref(parent_node),
                             self.manifest_ref(parent2_node))

            # TODO: this would benefit from less git queries
            changes = list(get_changes(commit, parents))

            files = [
                (path, mode, sha1)
                for mode, _, sha1, path in Git.ls_tree(commit, recursive=True)
            ]
            manifests = sorted_merge(parent_manifest,
                                     parent2_manifest,
                                     key=lambda i: i.path,
                                     non_key=lambda i: i)
            for line in sorted_merge(files, sorted_merge(changes, manifests)):
                path, f, (change, (manifest_line_p1, manifest_line_p2)) = line
                if not f:  # File was removed
                    if manifest_line_p1:
                        manifest.removed.add(path)
                        changeset_files.append(path)
                    continue
                mode, sha1 = f
                attr = self.ATTR[mode]
                if manifest_line_p1 and not manifest_line_p2:
                    file_parents = (manifest_line_p1.sha1, )
                elif manifest_line_p2 and not manifest_line_p1:
                    file_parents = (manifest_line_p2.sha1, )
                elif not manifest_line_p1 and not manifest_line_p2:
                    file_parents = ()
                elif manifest_line_p1.sha1 == manifest_line_p2.sha1:
                    file_parents = (manifest_line_p1.sha1, )
                else:
                    if self._merge_warn == 1:
                        logging.warning('This may take a while...')
                        self._merge_warn = 2
                    file_parents = (manifest_line_p1.sha1,
                                    manifest_line_p2.sha1)

                assert file_parents is not None
                f = self._create_file_internal(
                    sha1,
                    *file_parents,
                    git_manifest_parents=git_manifests,
                    path=path)
                file_parents = tuple(p for p in (f.parent1, f.parent2)
                                     if p != NULL_NODE_ID)
                merged = len(file_parents) == 2
                if not merged and file_parents:
                    if self.git_file_ref(file_parents[0]) == sha1:
                        node = file_parents[0]
                    else:
                        merged = True
                if merged:
                    node = self._store_file_internal(f)
                else:
                    node = file_parents[0]

                attr_change = (manifest_line_p1
                               and manifest_line_p1.attr != attr)
                manifest.add(path, node, attr, modified=merged or attr_change)
                if merged or attr_change:
                    changeset_files.append(path)
            if manifest.raw_data == parent_manifest.raw_data:
                return parent_manifest, []
            manifest.parents = (parent_node, parent2_node)
            return manifest, changeset_files

        def process_diff(diff):
            for (mode_before, mode_after, sha1_before, sha1_after, status,
                 path) in diff:
                if status[:1] == b'R':
                    yield status[1:], (b'000000', sha1_before, NULL_NODE_ID,
                                       b'D')
                yield path, (mode_after, sha1_before, sha1_after, status)

        git_diff = sorted(l for l in process_diff(
            GitHgHelper.diff_tree(parents[0], commit, detect_copy=True)))
        if not git_diff:
            return parent_manifest, []

        parent_lines = OrderedDict((l.path, l) for l in parent_manifest)
        items = manifest.items
        for line in sorted_merge(iteritems(parent_lines),
                                 git_diff,
                                 non_key=lambda i: i[1]):
            path, manifest_line, change = line
            if not change:
                items.append(manifest_line)
                continue
            mode_after, sha1_before, sha1_after, status = change
            path2 = status[1:]
            status = status[:1]
            attr = self.ATTR.get(mode_after)
            if status == b'D':
                manifest.removed.add(path)
                changeset_files.append(path)
                continue
            if status in b'MT':
                if sha1_before == sha1_after:
                    node = manifest_line.sha1
                else:
                    node = self.create_file(
                        sha1_after,
                        manifest_line.sha1,
                        git_manifest_parents=(
                            self.manifest_ref(parent_node), ),
                        path=path)
            elif status in b'RC':
                if sha1_after != EMPTY_BLOB:
                    node = self.create_copy(
                        (path2, parent_lines[path2].sha1),
                        sha1_after,
                        git_manifest_parents=(
                            self.manifest_ref(parent_node), ),
                        path=path)
                else:
                    node = self.create_file(
                        sha1_after,
                        git_manifest_parents=(
                            self.manifest_ref(parent_node), ),
                        path=path)
            else:
                assert status == b'A'
                node = self.create_file(
                    sha1_after,
                    git_manifest_parents=(self.manifest_ref(parent_node), ),
                    path=path)
            manifest.add(path, node, attr, modified=True)
            changeset_files.append(path)
        manifest.parents = (parent_node, )
        manifest.delta_node = parent_node
        return manifest, changeset_files
Exemple #6
0
 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)
Exemple #7
0
    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
            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
Exemple #8
0
 def __iter__(self):
     return util.iteritems(self._tags)
Exemple #9
0
    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)

        if self._metadata_sha1 and update_metadata and not refresh and \
                interval_expired('fsck', 86400 * 7):
            logging.warn('Have you run `git cinnabar fsck` recently?')
        GitHgHelper.close(rollback=False)
Exemple #10
0
    def push(self, *refspecs):
        try:
            default = b'never' if self._graft else b'phase'
            values = {
                None: default,
                b'': default,
                b'never': b'never',
                b'phase': b'phase',
                b'always': b'always',
            }
            data = Git.config('cinnabar.data', self._remote.name,
                              values=values)
        except InvalidConfig as e:
            logging.error(str(e))
            return 1

        pushes = list((Git.resolve_ref(fsdecode(s.lstrip(b'+'))), d,
                       s.startswith(b'+'))
                      for s, d in (r.split(b':', 1) for r in refspecs))
        if not self._repo.capable(b'unbundle'):
            for source, dest, force in pushes:
                self._helper.write(
                    b'error %s Remote does not support the "unbundle" '
                    b'capability\n' % dest)
            self._helper.write(b'\n')
            self._helper.flush()
        else:
            repo_heads = self._branchmap.heads()
            PushStore.adopt(self._store, self._graft)
            pushed = push(self._repo, self._store, pushes, repo_heads,
                          self._branchmap.names(), self._dry_run)

            status = {}
            for source, dest, _ in pushes:
                if dest.startswith(b'refs/tags/'):
                    if source:
                        status[dest] = b'Pushing tags is unsupported'
                    else:
                        status[dest] = \
                            b'Deleting remote tags is unsupported'
                    continue
                bookmark_prefix = strip_suffix(
                    (self._bookmark_template or b''), b'%s')
                if not bookmark_prefix or not dest.startswith(bookmark_prefix):
                    if source:
                        status[dest] = bool(len(pushed))
                    else:
                        status[dest] = \
                            b'Deleting remote branches is unsupported'
                    continue
                name = unquote_to_bytes(dest[len(bookmark_prefix):])
                if source:
                    source = self._store.hg_changeset(source)
                status[dest] = self._repo.pushkey(
                    b'bookmarks', name, self._bookmarks.get(name, b''),
                    source or b'')

            for source, dest, force in pushes:
                if status[dest] is True:
                    self._helper.write(b'ok %s\n' % dest)
                elif status[dest]:
                    self._helper.write(b'error %s %s\n' % (dest, status[dest]))
                else:
                    self._helper.write(b'error %s nothing changed on remote\n'
                                       % dest)
            self._helper.write(b'\n')
            self._helper.flush()

            if not pushed or self._dry_run:
                data = False
            elif data == b'always':
                data = True
            elif data == b'phase':
                phases = self._repo.listkeys(b'phases')
                drafts = {}
                if not phases.get(b'publishing', False):
                    drafts = set(p for p, is_draft in iteritems(phases)
                                 if int(is_draft))
                if not drafts:
                    data = True
                else:
                    def draft_commits():
                        for d in drafts:
                            c = self._store.changeset_ref(d)
                            if c:
                                yield b'^%s^@' % c
                        for h in pushed.heads():
                            yield h

                    args = [b'--ancestry-path', b'--topo-order']
                    args.extend(draft_commits())

                    pushed_drafts = tuple(
                        c for c, t, p in GitHgHelper.rev_list(*args))

                    # Theoretically, we could have commits with no
                    # metadata that the remote declares are public, while
                    # the rest of our push is in a draft state. That is
                    # however so unlikely that it's not worth the effort
                    # to support partial metadata storage.
                    data = not bool(pushed_drafts)
            elif data == b'never':
                data = False

            self._store.close(rollback=not data)
Exemple #11
0
    def import_(self, *refs):
        # If anything wrong happens at any time, we risk git picking
        # the existing refs/cinnabar refs, so remove them preventively.
        for sha1, ref in Git.for_each_ref('refs/cinnabar/refs/heads',
                                          'refs/cinnabar/hg',
                                          'refs/cinnabar/HEAD'):
            Git.delete_ref(ref)

        def resolve_head(head):
            resolved = self._refs.get(head)
            if resolved is None:
                return resolved
            if resolved.startswith(b'@'):
                return self._refs.get(resolved[1:])
            return resolved

        wanted_refs = {k: v for k, v in (
                       (h, resolve_head(h)) for h in refs) if v}
        heads = wanted_refs.values()
        if not heads:
            heads = self._branchmap.heads()

        try:
            # Mercurial can be an order of magnitude slower when creating
            # a bundle when not giving topological heads, which some of
            # the branch heads might not be.
            # http://bz.selenic.com/show_bug.cgi?id=4595
            # So, when we're pulling all branch heads, just ask for the
            # topological heads instead.
            # `heads` might contain known heads, if e.g. the remote has
            # never been pulled from, but we happen to have some of its
            # heads locally already.
            if self._has_unknown_heads:
                unknown_heads = self._branchmap.unknown_heads()
                if set(heads).issuperset(unknown_heads):
                    heads = set(self._branchmap.heads()) & unknown_heads
                getbundle(self._repo, self._store, heads,
                          self._branchmap.names())
        except Exception:
            wanted_refs = {}
            raise
        finally:
            for ref, value in iteritems(wanted_refs):
                ref = b'refs/cinnabar/' + ref
                Git.update_ref(ref, self._store.changeset_ref(value))

        self._store.close()

        self._helper.write(b'done\n')
        self._helper.flush()

        if self._remote.name and self._refs_style('heads'):
            if Git.config('fetch.prune', self._remote.name) != b'true':
                prune = 'remote.%s.prune' % fsdecode(self._remote.name)
                sys.stderr.write(
                    'It is recommended that you set "%(conf)s" or '
                    '"fetch.prune" to "true".\n'
                    '  git config %(conf)s true\n'
                    'or\n'
                    '  git config fetch.prune true\n'
                    % {'conf': prune}
                )

        if self._store.tag_changes:
            sys.stderr.write(
                '\nRun the following command to update tags:\n')
            sys.stderr.write('  git fetch --tags hg::tags: tag "*"\n')
Exemple #12
0
    def list(self, arg=None):
        assert not arg or arg == b'for-push'

        fetch = (Git.config('cinnabar.fetch') or b'').split()
        if fetch:
            heads = [unhexlify(f) for f in fetch]
            branchmap = {None: heads}
            bookmarks = {}

        elif self._repo.capable(b'batch'):
            if hasattr(self._repo, 'commandexecutor'):
                with self._repo.commandexecutor() as e:
                    branchmap = e.callcommand(b'branchmap', {})
                    heads = e.callcommand(b'heads', {})
                    bookmarks = e.callcommand(b'listkeys', {
                        b'namespace': b'bookmarks'
                    })
                branchmap = branchmap.result()
                heads = heads.result()
                bookmarks = bookmarks.result()
            elif hasattr(self._repo, b'iterbatch'):
                batch = self._repo.iterbatch()
                batch.branchmap()
                batch.heads()
                batch.listkeys(b'bookmarks')
                batch.submit()
                branchmap, heads, bookmarks = batch.results()
            else:
                batch = self._repo.batch()
                branchmap = batch.branchmap()
                heads = batch.heads()
                bookmarks = batch.listkeys(b'bookmarks')
                batch.submit()
                branchmap = branchmap.value
                heads = heads.value
                bookmarks = bookmarks.value
            if heads == [b'\0' * 20]:
                heads = []
        else:
            while True:
                branchmap = self._repo.branchmap()
                heads = self._repo.heads()
                if heads == [b'\0' * 20]:
                    heads = []
                # Some branch heads can be non-heads topologically, but if
                # some heads don't appear in the branchmap, then something
                # was pushed to the repo between branchmap() and heads()
                if set(heads).issubset(
                        set(chain(*(v for _, v in iteritems(branchmap))))):
                    break
            bookmarks = self._repo.listkeys(b'bookmarks')

        self._bookmarks = bookmarks
        branchmap = self._branchmap = BranchMap(self._store, branchmap,
                                                heads)
        self._has_unknown_heads = bool(self._branchmap.unknown_heads())
        if self._graft and self._has_unknown_heads and not arg:
            self._store.prepare_graft()
            get_heads = set(branchmap.heads()) & branchmap.unknown_heads()
            getbundle(self._repo, self._store, get_heads, branchmap.names())
            # We may have failed to graft all changesets, in which case we
            # skipped them. If that's what happened, we want to create a
            # new branchmap containing all we do know about, so that we can
            # avoid telling git about things we don't know, because if we
            # didn't, it would ask for them, and subsequently fail because
            # they are missing.
            # Since we can't know for sure what the right tips might be for
            # each branch, we won't expose the tips. This means we don't
            # need to care about the order of the heads for the new
            # branchmap.
            self._has_unknown_heads = any(not(self._store.changeset_ref(h))
                                          for h in get_heads)
            if self._has_unknown_heads:
                new_branchmap = {
                    branch: set(h for h in branchmap.heads(branch))
                    for branch in branchmap.names()
                }
                new_branchmap = {
                    branch: set(h for h in branchmap.heads(branch)
                                if h not in branchmap.unknown_heads())
                    for branch in branchmap.names()
                }
                new_heads = set(h for h in branchmap.heads()
                                if h not in branchmap.unknown_heads())
                for status, head, branch in self._store._hgheads.iterchanges():
                    branch_heads = new_branchmap.get(branch)
                    if status == VersionedDict.REMOVED:
                        if branch_heads and head in branch_heads:
                            branch_heads.remove(head)
                        if head in new_heads:
                            new_heads.remove(head)
                    else:
                        if not branch_heads:
                            branch_heads = new_branchmap[branch] = set()
                        branch_heads.add(head)
                        new_heads.add(head)

                branchmap = self._branchmap = BranchMap(
                    self._store, new_branchmap, list(new_heads))

        refs_style = None
        refs_styles = ('bookmarks', 'heads', 'tips')
        if not fetch and branchmap.heads():
            refs_config = 'cinnabar.refs'
            if arg == b'for-push':
                if Git.config('cinnabar.pushrefs', remote=self._remote.name):
                    refs_config = 'cinnabar.pushrefs'

            refs_style = ConfigSetFunc(refs_config, refs_styles,
                                       remote=self._remote.name,
                                       default='all')

        refs_style = refs_style or (lambda x: True)
        self._refs_style = refs_style

        refs = {}
        if refs_style('heads') or refs_style('tips'):
            if refs_style('heads') and refs_style('tips'):
                self._head_template = b'refs/heads/branches/%s/%s'
                self._tip_template = b'refs/heads/branches/%s/tip'
            elif refs_style('heads') and refs_style('bookmarks'):
                self._head_template = b'refs/heads/branches/%s/%s'
            elif refs_style('heads'):
                self._head_template = b'refs/heads/%s/%s'
            elif refs_style('tips') and refs_style('bookmarks'):
                self._tip_template = b'refs/heads/branches/%s'
            elif refs_style('tips'):
                self._tip_template = b'refs/heads/%s'

            for branch in sorted(branchmap.names()):
                branch_tip = branchmap.tip(branch)
                if refs_style('heads'):
                    for head in sorted(branchmap.heads(branch)):
                        if head == branch_tip and refs_style('tips'):
                            continue
                        refs[self._head_template % (branch, head)] = head
                if branch_tip and refs_style('tips'):
                    refs[self._tip_template % branch] = branch_tip

        if refs_style('bookmarks'):
            if refs_style('heads') or refs_style('tips'):
                self._bookmark_template = b'refs/heads/bookmarks/%s'
            else:
                self._bookmark_template = b'refs/heads/%s'
            for name, sha1 in sorted(iteritems(bookmarks)):
                if sha1 == NULL_NODE_ID:
                    continue
                ref = self._store.changeset_ref(sha1)
                if self._graft and not ref:
                    continue
                refs[self._bookmark_template % name] = sha1

        for f in fetch:
            refs[b'hg/revs/%s' % f] = f

        head_ref = None
        if refs_style('bookmarks') and b'@' in bookmarks:
            head_ref = self._bookmark_template % b'@'
        elif refs_style('tips'):
            head_ref = self._tip_template % b'default'
        elif refs_style('heads'):
            head_ref = self._head_template % (
                b'default', branchmap.tip(b'default'))

        if head_ref:
            head = refs.get(head_ref)
            if self._graft and head:
                head = self._store.changeset_ref(head)
            if head:
                refs[b'HEAD'] = b'@%s' % head_ref

        self._refs = {sanitize_branch_name(k): v
                      for k, v in iteritems(refs)}

        head_prefix = strip_suffix((self._head_template or b''), b'%s/%s')
        for k, v in sorted(iteritems(self._refs)):
            if head_prefix and k.startswith(head_prefix):
                v = self._store.changeset_ref(v) or self._branchmap.git_sha1(v)
            elif not v.startswith(b'@'):
                v = self._store.changeset_ref(v) or b'?'
            if not self._graft or v != b'?':
                self._helper.write(b'%s %s\n' % (v, k))

        self._helper.write(b'\n')
        self._helper.flush()
Exemple #13
0
def fsck_quick(force=False):
    status = FsckStatus()
    store = GitHgStore()

    metadata_commit = Git.resolve_ref('refs/cinnabar/metadata')
    if not metadata_commit:
        status.info('There does not seem to be any git-cinnabar metadata.\n'
                    'Is this a git-cinnabar clone?')
        return 1
    broken_metadata = Git.resolve_ref('refs/cinnabar/broken')
    checked_metadata = Git.resolve_ref('refs/cinnabar/checked')
    if checked_metadata == broken_metadata:
        checked_metadata = None
    if metadata_commit == checked_metadata and not force:
        status.info('The git-cinnabar metadata was already checked and is '
                    'presumably clean.\n'
                    'Try `--force` if you want to check anyways.')
        return 0
    elif force:
        checked_metadata = None

    commit = GitCommit(metadata_commit)
    if commit.body != b'files-meta unified-manifests-v2':
        status.info(
            'The git-cinnabar metadata is incompatible with this version.\n'
            'Please use the git-cinnabar version it was used with last.\n')
        return 1
    if len(commit.parents) > 6 or len(commit.parents) < 5:
        status.report('The git-cinnabar metadata seems to be corrupted in '
                      'unexpected ways.\n')
        return 1
    changesets, manifests, hg2git, git2hg, files_meta = commit.parents[:5]

    commit = GitCommit(changesets)
    heads = OrderedDict(
        (node, branch)
        for node, _, branch in (d.partition(b' ')
                                for d in commit.body.splitlines()))
    if len(heads) != len(commit.parents):
        status.report('The git-cinnabar metadata seems to be corrupted in '
                      'unexpected ways.\n')
        return 1

    manifest_nodes = []

    parents = None
    fix_changeset_heads = False

    def get_checked_metadata(num):
        if not checked_metadata:
            return None
        commit = Git.resolve_ref('{}^{}'.format(
            checked_metadata.decode('ascii'), num))
        if commit:
            return GitCommit(commit)

    checked_commit = get_checked_metadata(1)
    # TODO: Check that the recorded heads are actually dag heads.
    for c, changeset_node in progress_iter(
            'Checking {} changeset heads',
        ((c, node) for c, node in zip(commit.parents, heads)
         if not checked_commit or c not in checked_commit.parents)):
        gitsha1 = GitHgHelper.hg2git(changeset_node)
        if gitsha1 == NULL_NODE_ID:
            status.report('Missing hg2git metadata for changeset %s' %
                          changeset_node.decode('ascii'))
            continue
        if gitsha1 != c:
            if parents is None:
                parents = set(commit.parents)
            if gitsha1 not in parents:
                status.report('Inconsistent metadata:\n'
                              '  Head metadata says changeset %s maps to %s\n'
                              '  but hg2git metadata says it maps to %s' %
                              (changeset_node.decode('ascii'),
                               c.decode('ascii'), gitsha1.decode('ascii')))
                continue
            fix_changeset_heads = True
        changeset = store._changeset(c, include_parents=True)
        if not changeset:
            status.report('Missing git2hg metadata for git commit %s' %
                          c.decode('ascii'))
            continue
        if changeset.node != changeset_node:
            if changeset.node not in heads:
                status.report(
                    'Inconsistent metadata:\n'
                    '  Head metadata says %s maps to changeset %s\n'
                    '  but git2hg metadata says it maps to changeset %s' %
                    (c.decode('ascii'), changeset_node.decode('ascii'),
                     changeset.node.decode('ascii')))
                continue
            fix_changeset_heads = True
        if changeset.node != changeset.sha1:
            status.report('Sha1 mismatch for changeset %s' %
                          changeset.node.decode('ascii'))
            continue
        changeset_branch = changeset.branch or b'default'
        if heads[changeset.node] != changeset_branch:
            status.report(
                'Inconsistent metadata:\n'
                '  Head metadata says changeset %s is in branch %s\n'
                '  but git2hg metadata says it is in branch %s' %
                (changeset.node.decode('ascii'), fsdecode(
                    heads[changeset.node]), fsdecode(changeset_branch)))
            continue
        manifest_nodes.append(changeset.manifest)

    if status('broken'):
        return 1

    # Rebuilding manifests benefits from limiting the difference with
    # the last rebuilt manifest. Similarly, building the list of unique
    # files in all manifests benefits from that too.
    # Unfortunately, the manifest heads are not ordered in a topological
    # relevant matter, and the differences between two consecutive manifests
    # can be much larger than they could be. The consequence is spending a
    # large amount of time rebuilding the manifests and gathering the files
    # list. It's actually faster to attempt to reorder them according to
    # some heuristics first, such that the differences are smaller.
    # Here, we use the depth from the root node(s) to reorder the manifests.
    # This doesn't give the most optimal ordering, but it's already much
    # faster. On a clone of multiple mozilla-* repositories with > 1400 heads,
    # it's close to an order of magnitude difference on the "Checking
    # manifests" loop.
    depths = {}
    roots = set()
    manifest_queue = []
    revs = []
    revs.append(b'%s^@' % manifests)
    if checked_metadata:
        revs.append(b'^%s^2^@' % checked_metadata)
    for m, _, parents in progress_iter(
            'Loading {} manifests',
            GitHgHelper.rev_list(b'--topo-order', b'--reverse',
                                 b'--full-history', *revs)):
        manifest_queue.append((m, parents))
        for p in parents:
            if p not in depths:
                roots.add(p)
            depths[m] = max(depths.get(p, 0) + 1, depths.get(m, 0))

    if status('broken'):
        return 1

    # TODO: check that all manifest_nodes gathered above are available in the
    # manifests dag, and that the dag heads are the recorded heads.
    manifests_commit = GitCommit(manifests)
    checked_commit = get_checked_metadata(2)
    depths = [(depths.get(p, 0), p) for p in manifests_commit.parents
              if not checked_commit or p not in checked_commit.parents]
    manifests_commit_parents = [p for _, p in sorted(depths)]
    previous = None
    all_interesting = set()
    for m in progress_iter('Checking {} manifest heads',
                           manifests_commit_parents):
        c = GitCommit(m)
        if not SHA1_RE.match(c.body):
            status.report('Invalid manifest metadata in git commit %s' %
                          m.decode('ascii'))
            continue
        gitsha1 = GitHgHelper.hg2git(c.body)
        if gitsha1 == NULL_NODE_ID:
            status.report('Missing hg2git metadata for manifest %s' %
                          c.body.decode('ascii'))
            continue
        if not GitHgHelper.check_manifest(c.body):
            status.report('Sha1 mismatch for manifest %s' %
                          c.body.decode('ascii'))

        files = {}
        if previous:
            for _, _, before, after, d, path in GitHgHelper.diff_tree(
                    previous, m):
                if d in b'AM' and before != after and \
                        (path, after) not in all_interesting:
                    files[path] = after
        else:
            for _, t, sha1, path in GitHgHelper.ls_tree(m, recursive=True):
                if (path, sha1) not in all_interesting:
                    files[path] = sha1
        all_interesting.update(iteritems(files))
        previous = m

    if status('broken'):
        return 1

    # Don't check files that were already there in the previously checked
    # manifests.
    previous = None
    for r in roots:
        if previous:
            for _, _, before, after, d, path in GitHgHelper.diff_tree(
                    previous, r):
                if d in b'AM' and before != after:
                    all_interesting.discard((path, after))
        else:
            for _, t, sha1, path in GitHgHelper.ls_tree(r, recursive=True):
                all_interesting.discard((path, sha1))
        previous = r

    progress = Progress('Checking {} files')
    while all_interesting and manifest_queue:
        (m, parents) = manifest_queue.pop()
        changes = get_changes(m, parents, all=True)
        for path, hg_file, hg_fileparents in changes:
            if hg_fileparents[1:] == (hg_file, ):
                continue
            elif hg_fileparents[:1] == (hg_file, ):
                continue
            # Reaching here means the file received a modification compared
            # to its parents. If it's a file we're going to check below,
            # it means we don't need to check its parents if somehow they were
            # going to be checked. If it's not a file we're going to check
            # below, it's because it's either a file we weren't interested in
            # in the first place, or it's the parent of a file we have checked.
            # Either way, we aren't interested in the parents.
            for p in hg_fileparents:
                all_interesting.discard((path, p))
            if (path, hg_file) not in all_interesting:
                continue
            all_interesting.remove((path, hg_file))
            if not GitHgHelper.check_file(hg_file, *hg_fileparents):
                p = store.manifest_path(path)
                status.report('Sha1 mismatch for file %s\n'
                              '  revision %s' %
                              (fsdecode(p), hg_file.decode('ascii')))

                print_parents = ' '.join(
                    p.decode('ascii') for p in hg_fileparents
                    if p != NULL_NODE_ID)
                if print_parents:
                    status.report('  with parent%s %s' %
                                  ('s' if len(print_parents) > 41 else '',
                                   print_parents))
            progress.progress()
    progress.finish()
    if all_interesting:
        status.info('Could not find the following files:')
        for path, sha1 in sorted(all_interesting):
            p = store.manifest_path(path)
            status.info('  %s %s' % (sha1.decode('ascii'), fsdecode(p)))
        status.info('This might be a bug in `git cinnabar fsck`. Please open '
                    'an issue, with the message above, on\n'
                    'https://github.com/glandium/git-cinnabar/issues')
        return 1

    check_replace(store)

    if status('broken'):
        status.info('Your git-cinnabar repository appears to be corrupted.\n'
                    'Please open an issue, with the information above, on\n'
                    'https://github.com/glandium/git-cinnabar/issues')
        Git.update_ref(b'refs/cinnabar/broken', metadata_commit)
        if checked_metadata:
            status.info(
                '\nThen please try to run `git cinnabar rollback --fsck` to '
                'restore last known state, and to update from the mercurial '
                'repository.')
        else:
            status.info('\nThen please try to run `git cinnabar reclone`.')
        status.info(
            '\nPlease note this may affect the commit sha1s of mercurial '
            'changesets, and may require to rebase your local branches.')
        status.info(
            '\nAlternatively, you may start afresh with a new clone. In any '
            'case, please keep this corrupted repository around for further '
            'debugging.')
        return 1

    refresh = []
    if fix_changeset_heads:
        status.fix('Fixing changeset heads metadata order.')
        refresh.append('refs/cinnabar/changesets')
    interval_expired('fsck', 0)
    store.close(refresh=refresh)
    GitHgHelper._helper = False
    metadata_commit = Git.resolve_ref('refs/cinnabar/metadata')
    Git.update_ref(b'refs/cinnabar/checked', metadata_commit)
    return 0
Exemple #14
0
def check_replace(store):
    self_refs = [r for r, s in iteritems(store._replace) if r == s]
    for r in progress_iter('Removing {} self-referencing grafts', self_refs):
        del store._replace[r]