Esempio n. 1
0
    def _get_local_directory(self):
        repos_dir = pathlib.Path(user_cache_dir('forwardport'))
        repos_dir.mkdir(parents=True, exist_ok=True)
        repo_dir = repos_dir / self.repository.name

        if repo_dir.is_dir():
            return git(repo_dir)
        else:
            _logger.info("Cloning out %s to %s", self.repository.name,
                         repo_dir)
            subprocess.run([
                'git', 'clone', '--bare', 'https://{}:{}@github.com/{}'.format(
                    self.repository.project_id.fp_github_name,
                    self.repository.project_id.fp_github_token,
                    self.repository.name,
                ),
                str(repo_dir)
            ],
                           check=True)
            # add PR branches as local but namespaced (?)
            repo = git(repo_dir)
            # bare repos don't have a fetch spec by default (!) so adding one
            # removes the default behaviour and stops fetching the base
            # branches unless we add an explicit fetch spec for them
            repo.config('--add', 'remote.origin.fetch',
                        '+refs/heads/*:refs/heads/*')
            repo.config('--add', 'remote.origin.fetch',
                        '+refs/pull/*/head:refs/heads/pull/*')
            return repo
Esempio n. 2
0
def _cleanup_cache(config, users):
    """ forwardport has a repo cache which it assumes is unique per name
    but tests always use the same repo paths / names for different repos
    (the repos get re-created), leading to divergent repo histories.

    So clear cache after each test, two tests should not share repos.
    """
    yield
    cache_root = pathlib.Path(user_cache_dir('forwardport'))
    rmtree(cache_root / config['github']['owner'], ignore_errors=True)
    for login in users.values():
        rmtree(cache_root / login, ignore_errors=True)
Esempio n. 3
0
    def _create_fp_branch(self, target_branch, fp_branch_name, cleanup):
        """ Creates a forward-port for the current PR to ``target_branch`` under
        ``fp_branch_name``.

        :param target_branch: the branch to port forward to
        :param fp_branch_name: the name of the branch to create the FP under
        :param ExitStack cleanup: so the working directories can be cleaned up
        :return: (conflictp, working_copy)
        :rtype: (bool, Repo)
        """
        source = self._get_local_directory()
        # update all the branches & PRs
        _logger.info("Update %s", source._directory)
        source.with_params('gc.pruneExpire=1.day.ago').fetch('-p', 'origin')
        # FIXME: check that pr.head is pull/{number}'s head instead?
        source.cat_file(e=self.head)
        # create working copy
        _logger.info("Create working copy to forward-port %s:%d to %s",
                     self.repository.name, self.number, target_branch.name)
        working_copy = source.clone(cleanup.enter_context(
            tempfile.TemporaryDirectory(
                prefix='%s:%d-to-%s-' %
                (self.repository.name, self.number, target_branch.name),
                dir=user_cache_dir('forwardport'))),
                                    branch=target_branch.name)
        project_id = self.repository.project_id
        # add target remote
        working_copy.remote(
            'add', 'target',
            'https://{p.fp_github_name}:{p.fp_github_token}@github.com/{r.fp_remote_target}'
            .format(r=self.repository, p=project_id))
        _logger.info("Create FP branch %s", fp_branch_name)
        working_copy.checkout(b=fp_branch_name)

        root = self._get_root()
        try:
            root._cherry_pick(working_copy)
            return None, working_copy
        except CherrypickError as e:
            # using git diff | git apply -3 to get the entire conflict set
            # turns out to not work correctly: in case files have been moved
            # / removed (which turns out to be a common source of conflicts
            # when forward-porting) it'll just do nothing to the working copy
            # so the "conflict commit" will be empty
            # switch to a squashed-pr branch
            root_branch = 'origin/pull/%d' % root.number
            working_copy.checkout('-bsquashed', root_branch)
            root_commits = root.commits()

            to_tuple = operator.itemgetter('name', 'email', 'date')
            to_dict = lambda term, vals: {
                'GIT_%s_NAME' % term: vals[0],
                'GIT_%s_EMAIL' % term: vals[1],
                'GIT_%s_DATE' % term: vals[2],
            }
            authors, committers = set(), set()
            for c in (c['commit'] for c in root_commits):
                authors.add(to_tuple(c['author']))
                committers.add(to_tuple(c['committer']))
            fp_authorship = (project_id.fp_github_name,
                             project_id.fp_github_email, '')
            author = authors.pop() if len(authors) == 1 else fp_authorship
            committer = committers.pop() if len(
                committers) == 1 else fp_authorship
            conf = working_copy.with_config(
                env={
                    **to_dict('AUTHOR', author),
                    **to_dict('COMMITTER', committer),
                    'GIT_COMMITTER_DATE': '',
                })
            # squash to a single commit
            conf.reset('--soft', root_commits[0]['parents'][0]['sha'])
            conf.commit(a=True, message="temp")
            squashed = conf.stdout().rev_parse('HEAD').stdout.strip().decode()

            # switch back to the PR branch
            conf.checkout(fp_branch_name)
            # cherry-pick the squashed commit to generate the conflict
            conf.with_params('merge.renamelimit=0').with_config(
                check=False).cherry_pick(squashed)

            # if there was a single commit, reuse its message when committing
            # the conflict
            # TODO: still add conflict information to this?
            if len(root_commits) == 1:
                msg = root._make_fp_message(root_commits[0])
                conf.with_config(input=str(msg).encode())\
                    .commit(all=True, allow_empty=True, file='-')
            else:
                conf.commit(all=True,
                            allow_empty=True,
                            message="""Cherry pick of %s failed

stdout:
%s
stderr:
%s
""" % e.args)
            return e.args, working_copy
Esempio n. 4
0
    def _create_fp_branch(self, target_branch, fp_branch_name, cleanup):
        """ Creates a forward-port for the current PR to ``target_branch`` under
        ``fp_branch_name``.

        :param target_branch: the branch to port forward to
        :param fp_branch_name: the name of the branch to create the FP under
        :param ExitStack cleanup: so the working directories can be cleaned up
        :return: (conflictp, working_copy)
        :rtype: (bool, Repo)
        """
        source = self._get_local_directory()
        # update all the branches & PRs
        _logger.info("Update %s", source._directory)
        source.with_params('gc.pruneExpire=1.day.ago').fetch('-p', 'origin')
        # FIXME: check that pr.head is pull/{number}'s head instead?
        source.cat_file(e=self.head)
        # create working copy
        _logger.info("Create working copy to forward-port %s:%d to %s",
                     self.repository.name, self.number, target_branch.name)
        working_copy = source.clone(cleanup.enter_context(
            tempfile.TemporaryDirectory(
                prefix='%s:%d-to-%s' %
                (self.repository.name, self.number, target_branch.name),
                dir=user_cache_dir('forwardport'))),
                                    branch=target_branch.name)
        project_id = self.repository.project_id
        # configure local repo so commits automatically pickup bot identity
        working_copy.config('--local', 'user.name', project_id.fp_github_name)
        working_copy.config('--local', 'user.email',
                            project_id.fp_github_email)
        # add target remote
        working_copy.remote(
            'add', 'target',
            'https://{p.fp_github_name}:{p.fp_github_token}@github.com/{r.fp_remote_target}'
            .format(r=self.repository, p=project_id))
        _logger.info("Create FP branch %s", fp_branch_name)
        working_copy.checkout(b=fp_branch_name)

        root = self._get_root()
        try:
            root._cherry_pick(working_copy)
        except CherrypickError as e:
            # using git diff | git apply -3 to get the entire conflict set
            # turns out to not work correctly: in case files have been moved
            # / removed (which turns out to be a common source of conflicts
            # when forward-porting) it'll just do nothing to the working copy
            # so the "conflict commit" will be empty
            # switch to a squashed-pr branch
            root_branch = 'origin/pull/%d' % root.number
            working_copy.checkout('-bsquashed', root_branch)
            root_commits = root.commits()

            # squash to a single commit
            working_copy.reset('--soft', root_commits[0]['parents'][0]['sha'])
            working_copy.commit(a=True, message="temp")
            squashed = working_copy.stdout().rev_parse(
                'HEAD').stdout.strip().decode()

            # switch back to the PR branch
            working_copy.checkout(fp_branch_name)
            # cherry-pick the squashed commit
            working_copy.with_params('merge.renamelimit=0').with_config(
                check=False).cherry_pick(squashed)

            # if there was a single commit, reuse its message when committing
            # the conflict
            # TODO: still add conflict information to this?
            if len(root_commits) == 1:
                working_copy.commit(all=True,
                                    allow_empty=True,
                                    reuse_message=root_commits[0]['sha'])
            else:
                working_copy.commit(all=True,
                                    allow_empty=True,
                                    message="""Cherry pick of %s failed

stdout:
%s
stderr:
%s
""" % e.args)
            return e.args, working_copy
        else:
            return None, working_copy