示例#1
0
def synthesize_range(
    toolbox: SynthesizeLoopToolbox, synthesizer: AbstractSynthesizer
) -> None:
    # Loop through all the individual source versions to see which ones triggered a change.
    # version_ranges is a stack.  The code below maintains the invariant
    # that it's sorted with the oldest ranges being popped first.
    # That way, we apply changes to the current branch in order from oldest
    # to youngest.
    version_ranges: typing.List[typing.Tuple[int, int]] = [
        (0, len(toolbox.versions) - 1)
    ]
    while version_ranges:
        old, young = version_ranges.pop()
        if young == old + 1:
            # The base case: Found a version that triggered a change.
            toolbox.patch_merge_version(young)
            continue
        if not toolbox.git_branches_differ(
            toolbox.sub_branch(old), toolbox.sub_branch(young)
        ):
            continue  # No difference; no need to search range.
        # Select the middle version to synthesize.
        middle = (young - old) // 2 + old
        toolbox.synthesize_version_in_new_branch(synthesizer, middle)
        version_ranges.append((middle, young))
        version_ranges.append((old, middle))
示例#2
0
def synthesize_inner_loop(
    toolbox: SynthesizeLoopToolbox,
    synthesizer: AbstractSynthesizer,
):
    # Synthesize with the most recent version of all the sources.
    if not toolbox.synthesize_version_in_new_branch(synthesizer,
                                                    len(toolbox.versions) - 1):
        return  # No differences, nothing more to do.

    # Synthesize with the oldest version of all the sources.
    if 1 == len(toolbox.versions) or toolbox.synthesize_version_in_new_branch(
            synthesizer, 0):
        comment = """changes without context

        autosynth cannot find the source of changes triggered by earlier changes in this
        repository, or by version upgrades to tools such as linters."""
        toolbox.patch_merge_version(0, comment)

    # Binary search the range.
    synthesize_range(toolbox, synthesizer)
示例#3
0
def synthesize_loop_single_pr(
    toolbox: SynthesizeLoopToolbox,
    change_pusher: AbstractChangePusher,
    synthesizer: AbstractSynthesizer,
) -> int:
    """Loops through all source versions and creates a commit for every version
    changed that caused a change in the generated code.

    This function creates a single pull request for all sources.
    Arguments:
        toolbox {SynthesizeLoopToolbox} -- a toolbox
        change_pusher {AbstractChangePusher} -- Used to push changes to github.
        synthesizer {AbstractSynthesizer} -- Invokes synthesize.

    Returns:
        int -- Number of commits committed to this repo.
    """
    if change_pusher.check_if_pr_already_exists(toolbox.branch):
        return 0
    synthesize_inner_loop(toolbox, synthesizer)
    toolbox.push_changes(change_pusher)
    return toolbox.commit_count
示例#4
0
def synthesize_loop(
    toolbox: SynthesizeLoopToolbox,
    multiple_prs: bool,
    change_pusher: AbstractChangePusher,
    synthesizer: AbstractSynthesizer,
) -> int:
    """Loops through all source versions and creates a commit for every version
    changed that caused a change in the generated code.

    Arguments:
        toolbox {SynthesizeLoopToolbox} -- a toolbox
        multiple_prs {bool} -- True to create one pull request per source.
        change_pusher {AbstractChangePusher} -- Used to push changes to github.
        synthesizer {AbstractSynthesizer} -- Invokes synthesize.

    Returns:
        int -- Number of commits committed to this repo.
    """
    if not toolbox.versions:
        return 0  # No versions, nothing to synthesize.

    # Synthesize the library with the most recent versions of all sources.
    youngest = len(toolbox.versions) - 1
    has_changes = toolbox.synthesize_version_in_new_branch(
        synthesizer, youngest)
    if not has_changes:
        if (not toolbox.metadata_contains_generated_files(toolbox.branch)
                and toolbox.metadata_contains_generated_files(
                    toolbox.sub_branch(youngest)) and
                not change_pusher.check_if_pr_already_exists(toolbox.branch)):
            # Special case: the repo owner turned on obsolete file tracking.
            # Generate a one-time PR containing only metadata changes.
            executor.check_call(["git", "checkout", toolbox.branch])
            executor.check_call(
                ["git", "merge", "--squash",
                 toolbox.sub_branch(youngest)])
            pr_title = "chore: start tracking obsolete files"
            executor.check_call(["git", "commit", "-m", pr_title])
            pr = change_pusher.push_changes(1, toolbox.branch, pr_title)
            pr.add_labels(["context: full"])
            return 1
        return 0  # No changes, nothing to do.

    try:
        if multiple_prs:
            commit_count = 0
            for fork in toolbox.fork():
                if change_pusher.check_if_pr_already_exists(fork.branch):
                    continue
                executor.check_call(["git", "checkout", fork.branch])
                synthesize_inner_loop(fork, synthesizer)
                commit_count += fork.commit_count
                if fork.source_name == "self" or fork.count_commits_with_context(
                ) > 0:
                    fork.push_changes(change_pusher)
            return commit_count
    except Exception as e:
        logger.error(e)
        # Fallback to the single_pr loop to try to make some progress.
        synthesize_loop_single_pr(toolbox, change_pusher, synthesizer)
        # But still report the failure.
        raise

    return synthesize_loop_single_pr(toolbox, change_pusher, synthesizer)
示例#5
0
def _inner_main(temp_dir: str) -> int:
    """
    Returns:
        int -- Number of commits committed to the repo.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("--github-user", default=os.environ.get("GITHUB_USER"))
    parser.add_argument("--github-email",
                        default=os.environ.get("GITHUB_EMAIL"))
    parser.add_argument("--github-token",
                        default=os.environ.get("GITHUB_TOKEN"))
    parser.add_argument("--repository",
                        default=os.environ.get("REPOSITORY"),
                        required=True)
    parser.add_argument(
        "--synth-path",
        default=os.environ.get("SYNTH_PATH"),
        help=
        "If specified, changes the directory from which synthtool is invoked.",
    )
    parser.add_argument(
        "--synth-file-name",
        default=os.environ.get("SYNTH_FILE_NAME"),
        help=
        "If specified, override the synth file name and may be a path to a file. Defaults to 'synth.py'.",
    )
    parser.add_argument("--metadata-path",
                        default=os.environ.get("METADATA_PATH"))
    parser.add_argument("--base-log-dir", default="")
    parser.add_argument(
        "--deprecated-execution",
        default=False,
        action="store_true",
        help=
        "If specified, execute synth.py directly instead of synthtool. This behavior is deprecated.",
    )
    parser.add_argument("--branch-suffix",
                        default=os.environ.get("BRANCH_SUFFIX", None))
    parser.add_argument("--pr-title", default="")
    parser.add_argument("extra_args", nargs=argparse.REMAINDER)

    args = parser.parse_args()

    gh = github.GitHub(args.github_token)

    branch = "-".join(filter(None, ["autosynth", args.branch_suffix]))

    pr_title = args.pr_title or (
        f"[CHANGE ME] Re-generated {args.synth_path or ''} to pick up changes in "
        f"the API or client library generator.")
    change_pusher: AbstractChangePusher = ChangePusher(args.repository, gh,
                                                       branch)
    synth_file_name = args.synth_file_name or "synth.py"

    # capture logs for later
    # The logs directory path will be rendered in Sponge and Fusion as the test name,
    # so drop all the unimportant parts.
    base_log_dir = (pathlib.Path(args.base_log_dir) if args.base_log_dir else
                    pathlib.Path(os.getcwd()) / "logs")
    base_synth_log_path = (
        base_log_dir / pathlib.Path(args.synth_path or args.repository).name)
    logger.info(f"logs will be written to: {base_synth_log_path}")

    working_repo_path = synthtool_git.clone(
        f"https://github.com/{args.repository}.git")

    try:
        os.chdir(working_repo_path)

        git.configure_git(args.github_user, args.github_email)

        git.setup_branch(branch)

        if args.synth_path:
            os.chdir(args.synth_path)

        metadata_path = os.path.join(args.metadata_path or "",
                                     "synth.metadata")

        flags = autosynth.flags.parse_flags(synth_file_name)
        # Override flags specified in synth.py with flags specified in environment vars.
        for key in flags.keys():
            env_value = os.environ.get(key, "")
            if env_value:
                flags[key] = False if env_value.lower(
                ) == "false" else env_value

        metadata = load_metadata(metadata_path)
        multiple_commits = flags[autosynth.flags.AUTOSYNTH_MULTIPLE_COMMITS]
        multiple_prs = flags[autosynth.flags.AUTOSYNTH_MULTIPLE_PRS]
        if (not multiple_commits and not multiple_prs) or not metadata:
            if change_pusher.check_if_pr_already_exists(branch):
                return 0

            synth_log_path = base_synth_log_path
            for arg in args.extra_args:
                synth_log_path = synth_log_path / arg

            synth_log = Synthesizer(
                metadata_path,
                args.extra_args,
                deprecated_execution=args.deprecated_execution,
                synth_py_path=synth_file_name,
            ).synthesize(synth_log_path / "sponge_log.log")

            if not has_changes():
                logger.info("No changes. :)")
                sys.exit(EXIT_CODE_SKIPPED)

            git.commit_all_changes(pr_title)
            change_pusher.push_changes(1, branch, pr_title, synth_log)
            return 1

        else:
            if not multiple_prs and change_pusher.check_if_pr_already_exists(
                    branch):
                return 0  # There's already an existing PR

            # Enumerate the versions to loop over.
            sources = metadata.get("sources", [])
            source_versions = [
                git_source.enumerate_versions_for_working_repo(
                    metadata_path, sources)
            ]
            # Add supported source version types below:
            source_versions.extend(
                git_source.enumerate_versions(sources, pathlib.Path(temp_dir)))

            # Prepare to call synthesize loop.
            synthesizer = Synthesizer(
                metadata_path,
                args.extra_args,
                deprecated_execution=args.deprecated_execution,
                synth_py_path=synth_file_name,
            )
            x = SynthesizeLoopToolbox(
                source_versions,
                branch,
                temp_dir,
                metadata_path,
                args.synth_path,
                base_synth_log_path,
            )
            if not multiple_commits:
                change_pusher = SquashingChangePusher(change_pusher)

            # Call the loop.
            commit_count = synthesize_loop(x, multiple_prs, change_pusher,
                                           synthesizer)

            if commit_count == 0:
                logger.info("No changes. :)")
                sys.exit(EXIT_CODE_SKIPPED)

            return commit_count
    finally:
        if args.synth_path:
            # We're generating code in a mono repo.  The state left behind will
            # probably be useful for generating the next API.
            pass
        else:
            # We're generating a single API in a single repo, and using a different
            # repo to generate the next API.  So the next synth will not be able to
            # use any of this state.  Clean it up to avoid running out of disk space.
            executor.run(["git", "clean", "-fdx"], cwd=working_repo_path)