예제 #1
0
def prompt_for_confirmation(context: Context, fail_title: str, message: str,
                            prompt: str):
    result = Result()

    if context.batch:
        result.value = context.assume_yes
        if not result.value:
            sys.stdout.write(prompt + ' -' + os.linesep)
            result.fail(const.EX_ABORTED, fail_title,
                        _("Operation aborted in batch mode."))
    else:
        if message is not None:
            cli.warn(message)
        sys.stderr.flush()
        sys.stdout.flush()

        if context.assume_yes:
            sys.stdout.write(prompt + ' y' + os.linesep)
            result.value = True
        else:
            result.value = cli.query_yes_no(sys.stdout, prompt, "no")

        if result.value is not True:
            result.error(const.EX_ABORTED_BY_USER, fail_title,
                         _("Operation aborted."), False)

    return result
예제 #2
0
class AbstractContext(object):
    result: Result = None

    def __init__(self):
        self.result = Result()

    def warn(self, message, reason):
        self.result.warn(message, reason)

    def error(self, exit_code, message, reason, throw: bool = False):
        self.result.error(exit_code, message, reason, throw)

    def fail(self, exit_code, message, reason):
        self.result.fail(exit_code, message, reason)

    def add_subresult(self, subresult):
        self.result.add_subresult(subresult)

    def has_errors(self):
        return self.result.has_errors()

    def abort_on_error(self):
        return self.result.abort_on_error()

    def abort(self):
        return self.result.abort()
예제 #3
0
def update_project_property_file(context: Context, prev_properties: dict,
                                 new_version: str, new_sequential_version: int,
                                 commit_out: CommitInfo):
    result = Result()
    result.value = False

    if context.config.property_file is not None:
        property_reader = PropertyIO.get_instance_by_filename(
            context.config.property_file)
        if property_reader is None:
            result.fail(
                os.EX_DATAERR,
                _("Property file not supported: {path}\n"
                  "Currently supported:\n"
                  "{listing}").format(path=repr(context.config.property_file),
                                      listing='\n'.join(
                                          ' - ' + type
                                          for type in ['*.properties'])), None)

        properties = update_project_properties(context, prev_properties,
                                               new_version,
                                               new_sequential_version)

        property_reader.write_file(context.config.property_file, properties)
        commit_out.add_file(context.config.property_file)
        result.value = True
    else:
        properties = None

    var_separator = ' : '

    if properties is not None:

        def log_property(properties: dict, key: str):
            if key is not None:
                commit_out.add_message(
                    '#properties[' + utils.quote(key, '"') + ']' +
                    var_separator + cli.if_none(properties.get(key), "null"))

        for property_key in [
                context.config.version_property,
                context.config.sequence_number_property
        ]:
            log_property(properties, property_key)

    if context.verbose and result.value != 0:
        print("properties have changed")
        print("commit message:")
        print(commit_out.message)

    return result
예제 #4
0
def git_for_line_or_fail(context: RepoContext,
                         result: Result,
                         command: list,
                         error_message: str = None,
                         error_reason: str = None):
    line = repotools.git_for_line(context, *command)
    if line is None:
        if error_message is not None:
            result.fail(os.EX_DATAERR, error_message, error_reason)
        else:
            result.fail(
                os.EX_DATAERR,
                _("git {sub_command} failed.").format(
                    sub_command=repr(utils.command_to_str(command))),
                error_reason)
    return line
예제 #5
0
def git_or_fail(context: RepoContext,
                result: Result,
                command: list,
                error_message: str = None,
                error_reason: str = None):
    returncode = git(context, command)
    if returncode != os.EX_OK:
        if error_message is not None:
            result.fail(os.EX_DATAERR, error_message, error_reason)
        else:
            first_command_token = next(
                filter(lambda token: not token.startswith('-'), command))
            result.fail(
                os.EX_DATAERR,
                _("git {sub_command} failed.").format(
                    sub_command=repr(first_command_token)), error_reason)
예제 #6
0
def cmd_convert_config(context):
    result = Result()

    with open(context.args['<input-file>'], mode='r', encoding='utf-8') as in_file:
        if in_file is None:
            result.fail(os.EX_USAGE,
                        _("Failed to open input file"),
                        None)
            return result
        input = PropertyIO.get_instance_by_filename(in_file.name)

        with open(context.args['<output-file>'], mode='w', encoding='utf-8') as out_file:
            if out_file is None:
                result.fail(os.EX_USAGE,
                            _("Failed to open output file"),
                            None)
                return result

            output = PropertyIO.get_instance_by_filename(out_file.name)
            config = input.from_stream(in_file)
            output.to_stream(out_file, config)
    return result
예제 #7
0
def clone_repository(context: Context, branch: str) -> Result:
    """
    :rtype: Result
    """
    result = Result()

    remote = repotools.git_get_remote(context.repo, context.config.remote_name)
    if remote is None:
        result.fail(
            os.EX_DATAERR, _("Failed to clone repo."),
            _("The remote {remote} does not exist.").format(
                remote=repr(context.config.remote_name)))

    tempdir_path = tempfile.mkdtemp(prefix=os.path.basename(context.repo.dir) +
                                    ".gitflow-clone.")
    try:
        if os.path.exists(tempdir_path):
            os.chmod(path=tempdir_path, mode=0o700)
            if os.path.isdir(tempdir_path):
                if os.listdir(tempdir_path):
                    result.fail(
                        os.EX_DATAERR, _("Failed to clone repo."),
                        _("Directory is not empty: {path}").format(
                            path=tempdir_path))
            else:
                result.fail(
                    os.EX_DATAERR, _("Failed to clone repo."),
                    _("File is not a directory: {path}").format(
                        path=tempdir_path))
        else:
            result.fail(
                os.EX_DATAERR, _("Failed to clone repo."),
                _("File does not exist: {path}").format(path=tempdir_path))

        if context.config.push_to_local:
            returncode, out, err = repotools.git_raw(git=context.repo.git,
                                                     args=[
                                                         'clone', '--branch',
                                                         branch, '--shared',
                                                         context.repo.dir,
                                                         tempdir_path
                                                     ],
                                                     verbose=context.verbose)
        else:
            returncode, out, err = repotools.git_raw(git=context.repo.git,
                                                     args=[
                                                         'clone', '--branch',
                                                         branch, '--reference',
                                                         context.repo.dir,
                                                         remote.url,
                                                         tempdir_path
                                                     ],
                                                     verbose=context.verbose)

        if returncode != os.EX_OK:
            result.error(os.EX_DATAERR, _("Failed to clone the repository."),
                         _("An unexpected error occurred."))
    except:
        result.error(os.EX_DATAERR, _("Failed to clone the repository."),
                     _("An unexpected error occurred."))
    finally:
        context.add_subresult(result)

    if not result.has_errors():
        repo = RepoContext()
        repo.git = context.repo.git
        repo.dir = tempdir_path
        repo.verbose = context.repo.verbose
        result.value = repo
    else:
        shutil.rmtree(path=tempdir_path)

    return result
예제 #8
0
    def create(args: dict, result_out: Result) -> 'Context':
        context = Context()
        context.config: Config = Config()

        if args is not None:
            context.args = args

            context.batch = context.args['--batch']
            context.assume_yes = context.args.get('--assume-yes')
            context.dry_run = context.args.get('--dry-run')
            # TODO remove this workaround
            context.verbose = (context.args['--verbose'] + 1) // 2
            context.pretty = context.args['--pretty']
        else:
            context.args = dict()

        # configure CLI
        cli.set_allow_color(not context.batch)

        # initialize repo context and attempt to load the config file
        if '--root' in context.args and context.args['--root'] is not None:
            context.root = context.args['--root']

            context.repo = RepoContext()
            context.repo.dir = context.root
            context.repo.verbose = context.verbose

            context.git_version = repotools.git_version(context.repo)
            # context.repo.use_root_dir_arg = semver.compare(context.git_version, "2.9.0") >= 0
            context.repo.use_root_dir_arg = False

            repo_root = repotools.git_rev_parse(context.repo, '--show-toplevel')

            # None when invalid or bare
            if repo_root is not None:
                context.repo.dir = repo_root

                if context.verbose >= const.TRACE_VERBOSITY:
                    cli.print("--------------------------------------------------------------------------------")
                    cli.print("refs in {repo}:".format(repo=context.repo.dir))
                    cli.print("--------------------------------------------------------------------------------")
                    for ref in repotools.git_list_refs(context.repo):
                        cli.print(repr(ref))
                    cli.print("--------------------------------------------------------------------------------")
                config_dir = context.repo.dir
            else:
                context.repo = None
                config_dir = context.root

            gitflow_config_file: Optional[str] = None
            if context.args['--config'] is not None:
                gitflow_config_file = os.path.join(config_dir, context.args['--config'])
                if gitflow_config_file is None:
                    result_out.fail(os.EX_DATAERR,
                                    _("the specified config file does not exist or is not a regular file: {path}.")
                                    .format(path=repr(gitflow_config_file)),
                                    None
                                    )
            else:
                for config_filename in const.DEFAULT_CONFIGURATION_FILE_NAMES:
                    path = os.path.join(config_dir, config_filename)
                    if os.path.exists(path):
                        gitflow_config_file = path
                        break
                if gitflow_config_file is None:
                    result_out.fail(os.EX_DATAERR,
                                    _("config file not found.")
                                    .format(path=repr(gitflow_config_file)),
                                    _("Default config files are\n:{list}")
                                    .format(list=const.DEFAULT_CONFIGURATION_FILE_NAMES)
                                    )

            if context.verbose >= const.TRACE_VERBOSITY:
                cli.print("gitflow_config_file: " + gitflow_config_file)

            with open(gitflow_config_file) as json_file:
                config = PropertyIO.get_instance_by_filename(gitflow_config_file).from_stream(json_file)
        else:
            config = object()

        build_config_json = config.get(const.CONFIG_BUILD)

        context.config.version_change_actions = config.get(const.CONFIG_ON_VERSION_CHANGE, [])

        context.config.build_stages = list()

        if build_config_json is not None:
            stages_json = build_config_json.get('stages')
            if stages_json is not None:
                for stage_key, stage_json in stages_json.items():

                    stage = BuildStage()

                    if isinstance(stage_json, dict):
                        stage.type = stage_json.get('type') or stage_key
                        if stage.type not in const.BUILD_STAGE_TYPES:
                            result_out.fail(
                                os.EX_DATAERR,
                                _("Configuration failed."),
                                _("Invalid build stage type {key}."
                                  .format(key=repr(stage.type)))
                            )

                        stage.name = stage_json.get('name') or stage_key

                        stage_labels = stage_json.get('labels')
                        if isinstance(stage_labels, list):
                            stage.labels.extend(stage_labels)
                        else:
                            stage.labels.append(stage_labels)

                        stage_steps_json = stage_json.get('steps')
                        if stage_steps_json is not None:
                            for step_key, step_json in stage_steps_json.items():
                                step = BuildStep()

                                if isinstance(step_json, dict):
                                    step.name = step_json.get('name') or step_key
                                    step.commands = step_json.get('commands')

                                    stage_labels = stage_json.get('labels')
                                    if isinstance(stage_labels, list):
                                        stage.labels.extend(stage_labels)
                                    else:
                                        stage.labels.append(stage_labels)
                                elif isinstance(step_json, list):
                                    step.name = step_key
                                    step.type = step_key
                                    step.commands = step_json
                                else:
                                    result_out.fail(
                                        os.EX_DATAERR,
                                        _("Configuration failed."),
                                        _("Invalid build step definition {type} {key}."
                                          .format(type=repr(type(step_json)), key=repr(step_key)))
                                    )

                                stage.steps.append(step)
                    elif isinstance(stage_json, list):
                        stage.type = stage_key
                        stage.name = stage_key

                        if len(stage_json):
                            step = BuildStep()
                            step.name = '#'
                            step.commands = stage_json
                            stage.steps.append(step)
                    else:
                        result_out.fail(
                            os.EX_DATAERR,
                            _("Configuration failed."),
                            _("Invalid build stage definition {key}."
                              .format(key=repr(stage_key)))
                        )
                    context.config.build_stages.append(stage)

        context.config.build_stages.sort(key=utils.cmp_to_key(lambda stage_a, stage_b:
                                                              const.BUILD_STAGE_TYPES.index(stage_a.type)
                                                              - const.BUILD_STAGE_TYPES.index(stage_b.type)
                                                              ),
                                         reverse=False
                                         )

        # project properties config

        context.config.property_file = config.get(const.CONFIG_PROJECT_PROPERTY_FILE)
        if context.config.property_file is not None:
            context.config.property_file = os.path.join(context.root, context.config.property_file)

        context.config.version_property = config.get(const.CONFIG_VERSION_PROPERTY)
        context.config.sequence_number_property = config.get(
            const.CONFIG_SEQUENCE_NUMBER_PROPERTY)
        context.config.version_property = config.get(
            const.CONFIG_VERSION_PROPERTY)

        property_names = [property for property in
                          [context.config.sequence_number_property, context.config.version_property] if
                          property is not None]
        duplicate_property_names = [item for item, count in collections.Counter(property_names).items() if count > 1]

        if len(duplicate_property_names):
            result_out.fail(os.EX_DATAERR, _("Configuration failed."),
                            _("Duplicate property names: {duplicate_property_names}").format(
                                duplicate_property_names=', '.join(duplicate_property_names))
                            )

        # version config

        context.config.version_config = VersionConfig()

        versioning_scheme = config.get(const.CONFIG_VERSIONING_SCHEME, const.DEFAULT_VERSIONING_SCHEME)

        if versioning_scheme not in const.VERSIONING_SCHEMES:
            result_out.fail(os.EX_DATAERR, _("Configuration failed."),
                            _("The versioning scheme {versioning_scheme} is invalid.").format(
                                versioning_scheme=utils.quote(versioning_scheme, '\'')))

        context.config.version_config.versioning_scheme = const.VERSIONING_SCHEMES[versioning_scheme]

        if context.config.version_config.versioning_scheme == VersioningScheme.SEMVER:
            qualifiers = config.get(const.CONFIG_VERSION_TYPES, const.DEFAULT_PRE_RELEASE_QUALIFIERS)
            if isinstance(qualifiers, str):
                qualifiers = [qualifier.strip() for qualifier in qualifiers.split(",")]
            if qualifiers != sorted(qualifiers):
                result_out.fail(
                    os.EX_DATAERR,
                    _("Configuration failed."),
                    _("Pre-release qualifiers are not specified in ascending order.")
                )
            context.config.version_config.qualifiers = qualifiers
            context.config.version_config.initial_version = const.DEFAULT_INITIAL_VERSION
        elif context.config.version_config.versioning_scheme == VersioningScheme.SEMVER_WITH_SEQ:
            context.config.version_config.qualifiers = None
            context.config.version_config.initial_version = const.DEFAULT_INITIAL_SEQ_VERSION
        else:
            context.fail(os.EX_CONFIG, "configuration error", "invalid versioning scheme")

        # branch config

        context.config.remote_name = "origin"
        context.config.release_branch_base = config.get(const.CONFIG_RELEASE_BRANCH_BASE,
                                                        const.DEFAULT_RELEASE_BRANCH_BASE)

        remote_prefix = repotools.create_ref_name(const.REMOTES_PREFIX, context.config.remote_name)

        context.release_base_branch_matcher = VersionMatcher(
            [const.LOCAL_BRANCH_PREFIX, remote_prefix],
            None,
            re.escape(context.config.release_branch_base),
        )

        context.release_branch_matcher = VersionMatcher(
            [const.LOCAL_BRANCH_PREFIX, remote_prefix],
            config.get(
                const.CONFIG_RELEASE_BRANCH_PREFIX,
                const.DEFAULT_RELEASE_BRANCH_PREFIX),
            config.get(
                const.CONFIG_RELEASE_BRANCH_PATTERN,
                const.DEFAULT_RELEASE_BRANCH_PATTERN),
        )

        context.work_branch_matcher = VersionMatcher(
            [const.LOCAL_BRANCH_PREFIX, remote_prefix],
            [const.BRANCH_PREFIX_DEV, const.BRANCH_PREFIX_PROD],
            config.get(
                const.CONFIG_WORK_BRANCH_PATTERN,
                const.DEFAULT_WORK_BRANCH_PATTERN),
        )

        context.version_tag_matcher = VersionMatcher(
            [const.LOCAL_TAG_PREFIX],
            config.get(
                const.CONFIG_VERSION_TAG_PREFIX,
                const.DEFAULT_VERSION_TAG_PREFIX),
            config.get(
                const.CONFIG_VERSION_TAG_PATTERN,
                const.DEFAULT_SEMVER_VERSION_TAG_PATTERN
                if context.config.version_config.versioning_scheme == VersioningScheme.SEMVER
                else const.DEFAULT_SEMVER_WITH_SEQ_VERSION_TAG_PATTERN)
        )
        context.version_tag_matcher.group_unique_code = None \
            if context.config.version_config.versioning_scheme == VersioningScheme.SEMVER \
            else 'prerelease_type'

        context.discontinuation_tag_matcher = VersionMatcher(
            [const.LOCAL_TAG_PREFIX],
            config.get(
                const.CONFIG_DISCONTINUATION_TAG_PREFIX,
                const.DEFAULT_DISCONTINUATION_TAG_PREFIX),
            config.get(
                const.CONFIG_DISCONTINUATION_TAG_PATTERN,
                const.DEFAULT_DISCONTINUATION_TAG_PATTERN),
            None
        )

        return context
예제 #9
0
def create_version_branch(command_context: CommandContext,
                          operation: Callable[[VersionConfig, Optional[str], Optional[int]], Result]) -> Result:
    result = Result()
    context: Context = command_context.context

    if command_context.selected_ref.name not in [
        repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, context.config.release_branch_base),
        repotools.create_ref_name(const.REMOTES_PREFIX,
                                  context.config.remote_name,
                                  context.config.release_branch_base)]:
        result.fail(os.EX_USAGE,
                    _("Failed to create release branch based on {branch}.")
                    .format(branch=repr(command_context.selected_ref.name)),
                    _("Release branches (major.minor) can only be created off {branch}")
                    .format(branch=repr(context.config.release_branch_base))
                    )

    existing_release_branches = list(repotools.git_list_refs(context.repo, repotools.ref_name([
        const.REMOTES_PREFIX,
        context.config.remote_name,
        'release'])))

    release_branch_merge_bases = dict()
    for release_branch in context.get_release_branches():
        merge_base = repotools.git_merge_base(context.repo, context.config.release_branch_base, release_branch)
        if merge_base is None:
            result.fail(os.EX_DATAERR,
                        "Failed to resolve merge base.",
                        None)
        branch_refs = release_branch_merge_bases.get(merge_base)
        if branch_refs is None:
            release_branch_merge_bases[merge_base] = branch_refs = list()
        branch_refs.append(release_branch)

    latest_branch = None
    branch_points_on_same_commit = list()
    subsequent_branches = list()

    for history_commit in repotools.git_list_commits(
            context=context.repo,
            start=None,
            end=command_context.selected_commit,
            options=const.BRANCH_COMMIT_SCAN_OPTIONS):
        branch_refs = release_branch_merge_bases.get(history_commit.obj_name)
        if branch_refs is not None and len(branch_refs):
            branch_refs = list(
                filter(lambda tag_ref: context.release_branch_matcher.format(tag_ref.name) is not None,
                       branch_refs))
            if not len(branch_refs):
                continue

            branch_refs.sort(
                reverse=True,
                key=utils.cmp_to_key(
                    lambda tag_ref_a, tag_ref_b: semver.compare(
                        context.release_branch_matcher.format(tag_ref_a.name),
                        context.release_branch_matcher.format(tag_ref_b.name)
                    )
                )
            )
            if latest_branch is None:
                latest_branch = branch_refs[0]
            if history_commit.obj_name == command_context.selected_commit:
                branch_points_on_same_commit.extend(branch_refs)
            # for tag_ref in tag_refs:
            #     print('<<' + tag_ref.name)
            break

    for history_commit in repotools.git_list_commits(context.repo,
                                                     command_context.selected_commit,
                                                     command_context.selected_ref):
        branch_refs = release_branch_merge_bases.get(history_commit.obj_name)
        if branch_refs is not None and len(branch_refs):
            branch_refs = list(
                filter(lambda tag_ref: context.release_branch_matcher.format(tag_ref.name) is not None,
                       branch_refs))
            if not len(branch_refs):
                continue

            branch_refs.sort(
                reverse=True,
                key=utils.cmp_to_key(
                    lambda tag_ref_a, tag_ref_b: semver.compare(
                        context.release_branch_matcher.format(tag_ref_a.name),
                        context.release_branch_matcher.format(tag_ref_b.name)
                    )
                )
            )
            # for tag_ref in tag_refs:
            #     print('>>' + tag_ref.name)
            subsequent_branches.extend(branch_refs)

    if context.verbose:
        cli.print("Branches on same commit:\n"
                  + '\n'.join(' - ' + repr(tag_ref.name) for tag_ref in branch_points_on_same_commit))

        cli.print("Subsequent branches:\n"
                  + '\n'.join(' - ' + repr(tag_ref.name) for tag_ref in subsequent_branches))

    if latest_branch is not None:
        latest_branch_version = context.release_branch_matcher.format(latest_branch.name)
        latest_branch_version_info = semver.parse_version_info(latest_branch_version)
    else:
        latest_branch_version = None
        latest_branch_version_info = None

    if latest_branch_version is not None:
        version_result = operation(context.config.version_config, latest_branch_version,
                                   get_global_sequence_number(context))
        result.add_subresult(version_result)

        new_version = version_result.value
        new_version_info = semver.parse_version_info(new_version)
    else:
        new_version_info = semver.parse_version_info(context.config.version_config.initial_version)
        new_version = version.format_version_info(new_version_info)

    scheme_procedures.get_sequence_number(context.config.version_config, new_version_info)

    if context.config.sequential_versioning:
        new_sequential_version = create_sequence_number_for_version(context, new_version)
    else:
        new_sequential_version = None

    try:
        config_in_selected_commit = read_config_in_commit(context.repo, command_context.selected_commit)
    except FileNotFoundError:
        config_in_selected_commit = dict()

    try:
        properties_in_selected_commit = read_properties_in_commit(context,
                                                                  context.repo,
                                                                  config_in_selected_commit,
                                                                  command_context.selected_commit)
    except FileNotFoundError:
        properties_in_selected_commit = dict()

    if not context.config.allow_shared_release_branch_base and len(branch_points_on_same_commit):
        result.fail(os.EX_USAGE,
                    _("Branch creation failed."),
                    _("Release branches cannot share a common ancestor commit.\n"
                      "Existing branches on commit {commit}:\n"
                      "{listing}")
                    .format(commit=command_context.selected_commit,
                            listing='\n'.join(' - ' + repr(tag_ref.name) for tag_ref in branch_points_on_same_commit)))

    if len(subsequent_branches):
        result.fail(os.EX_USAGE,
                    _("Branch creation failed."),
                    _("Subsequent release branches in history: %s\n")
                    % '\n'.join(' - ' + repr(tag_ref.name) for tag_ref in subsequent_branches))

    if context.config.tie_sequential_version_to_semantic_version \
            and len(existing_release_branches):
        prompt_result = prompt_for_confirmation(
            context=context,
            fail_title=_("Failed to create release branch based on {branch}.")
                .format(branch=repr(command_context.selected_ref.name)),
            message=_("This operation disables version increments "
                      "on all existing release branches.\n"
                      "Affected branches are:\n"
                      "{listing}")
                .format(listing=os.linesep.join(repr(branch.name) for branch in existing_release_branches))
            if not context.config.commit_version_property
            else _("This operation disables version increments on all existing branches.\n"
                   "Affected branches are:\n"
                   "{listing}")
                .format(listing=os.linesep.join(repr(branch.name) for branch in existing_release_branches)),
            prompt=_("Continue?"),
        )
        result.add_subresult(prompt_result)
        if result.has_errors() or not prompt_result.value:
            return result

    if not result.has_errors():
        if new_version is None:
            result.error(os.EX_SOFTWARE,
                         _("Internal error."),
                         _("Missing result version.")
                         )
        if latest_branch_version is not None and semver.compare(latest_branch_version, new_version) >= 0:
            result.error(os.EX_DATAERR,
                         _("Failed to increment version from {current_version} to {new_version}.")
                         .format(current_version=repr(latest_branch_version), new_version=repr(new_version)),
                         _("The new version is lower than or equal to the current version.")
                         )
        result.abort_on_error()

        branch_name = get_branch_name_for_version(context, new_version_info)
        tag_name = get_tag_name_for_version(context, new_version_info)

        clone_result = clone_repository(context, context.config.release_branch_base)
        cloned_repo: RepoContext = clone_result.value

        # run version change hooks on new release branch
        git_or_fail(cloned_repo, result, ['checkout', '--force',
                                          '-b', branch_name,
                                          command_context.selected_commit],
                    _("Failed to check out release branch."))

        clone_context: Context = create_temp_context(context, result, cloned_repo.dir)
        clone_context.config.remote_name = 'origin'

        commit_info = CommitInfo()
        commit_info.add_message("#version: " + cli.if_none(new_version))

        if (context.config.commit_version_property and new_version is not None) \
                or (context.config.commit_sequential_version_property and new_sequential_version is not None):

            update_result = update_project_property_file(clone_context,
                                                         properties_in_selected_commit,
                                                         new_version,
                                                         new_sequential_version,
                                                         commit_info)
            result.add_subresult(update_result)
            if result.has_errors():
                result.fail(os.EX_DATAERR,
                            _("Property update failed."),
                            _("An unexpected error occurred.")
                            )

        if new_version is not None:
            execute_version_change_actions(clone_context, latest_branch_version, new_version)

        if commit_info is not None:
            if command_context.selected_commit != command_context.selected_ref.target.obj_name:
                result.fail(os.EX_USAGE,
                            _("Failed to commit version update."),
                            _("The selected parent commit {commit} does not represent the tip of {branch}.")
                            .format(commit=command_context.selected_commit,
                                    branch=repr(command_context.selected_ref.name))
                            )

            # commit changes
            commit_info.add_parent(command_context.selected_commit)
            object_to_tag = create_commit(clone_context, result, commit_info)
        else:
            object_to_tag = command_context.selected_commit

        # show info and prompt for confirmation
        cli.print("ref                 : " + cli.if_none(command_context.selected_ref.name))
        cli.print("ref_" + const.DEFAULT_VERSION_VAR_NAME + "         : " + cli.if_none(latest_branch_version))
        cli.print("new_branch          : " + cli.if_none(branch_name))
        cli.print("new_" + const.DEFAULT_VERSION_VAR_NAME + "         : " + cli.if_none(new_version))
        cli.print("selected object     : " + cli.if_none(command_context.selected_commit))
        cli.print("tagged object       : " + cli.if_none(object_to_tag))

        prompt_result = prompt_for_confirmation(
            context=context,
            fail_title=_("Failed to create release branch based on {branch}.")
                .format(branch=repr(command_context.selected_ref.name)),
            message=_("The branch and tags are about to be pushed."),
            prompt=_("Continue?"),
        )
        result.add_subresult(prompt_result)
        if result.has_errors() or not prompt_result.value:
            return result

        # push atomically
        push_command = ['push', '--atomic']
        if context.dry_run:
            push_command.append('--dry-run')
        if context.verbose:
            push_command.append('--verbose')
        push_command.append(context.config.remote_name)

        # push the base branch commit
        # push_command.append(commit + ':' + const.LOCAL_BRANCH_PREFIX + selected_ref.local_branch_name)

        # push the new branch or fail if it exists
        push_command.extend(
            ['--force-with-lease=' + repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, branch_name) + ':',
             repotools.ref_target(object_to_tag) + ':' + repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX,
                                                                                   branch_name)])
        # push the new version tag or fail if it exists
        push_command.extend(['--force-with-lease=' + repotools.create_ref_name(const.LOCAL_TAG_PREFIX, tag_name) + ':',
                             repotools.ref_target(object_to_tag) + ':' + repotools.create_ref_name(
                                 const.LOCAL_TAG_PREFIX, tag_name)])

        git_or_fail(cloned_repo, result, push_command, _("Failed to push."))

    return result
예제 #10
0
def create_version_tag(command_context: CommandContext,
                       operation: Callable[[VersionConfig, Optional[str], Optional[int]], Result]) -> Result:
    result = Result()
    context: Context = command_context.context

    release_branches = command_context.context.get_release_branches(reverse=True)

    # TODO configuration
    allow_merge_base_tags = True  # context.config.allow_shared_release_branch_base

    selected_branch = command_context.selected_ref
    selected_branch_base_version = context.release_branch_matcher.format(command_context.selected_ref.name)
    if selected_branch_base_version is not None:
        selected_branch_base_version_info = semver.parse_version_info(selected_branch_base_version)
    else:
        selected_branch_base_version_info = None

    if selected_branch_base_version is None:
        result.fail(os.EX_USAGE,
                    _("Cannot bump version."),
                    _("{branch} is not a release branch.")
                    .format(branch=repr(command_context.selected_ref.name)))

    latest_version_tag = None
    preceding_version_tag = None
    preceding_branch_version_tag = None
    version_tags_on_same_commit = list()
    subsequent_version_tags = list()
    enclosing_versions = set()

    # abort scan, when a preceding commit for each tag type has been processed.
    # enclosing_versions now holds enough information for operation validation,
    # assuming the branch has not gone haywire in earlier commits
    # TODO evaluate upper and lower bound version for efficiency
    abort_version_scan = False

    on_selected_branch = False

    before_commit = False
    before_selected_branch = False

    for release_branch in release_branches:
        # fork_point = repotools.git_merge_base(context.repo, context.config.release_branch_base,
        #                                       command_context.selected_commit)
        # if fork_point is None:
        #     result.fail(os.EX_USAGE,
        #                 _("Cannot bump version."),
        #                 _("{branch} has no fork point on {base_branch}.")
        #                 .format(branch=repr(command_context.selected_ref.name),
        #                         base_branch=repr(context.config.release_branch_base)))
        fork_point = None

        branch_base_version = context.release_branch_matcher.format(release_branch.name)
        if branch_base_version is not None:
            branch_base_version_info = semver.parse_version_info(branch_base_version)
        else:
            branch_base_version_info = None

        on_selected_branch = not before_selected_branch and release_branch.name == selected_branch.name

        for history_commit in repotools.git_list_commits(
                context=context.repo,
                start=fork_point,
                end=release_branch.obj_name,
                options=const.BRANCH_COMMIT_SCAN_OPTIONS):
            at_commit = not before_commit and on_selected_branch and history_commit.obj_name == command_context.selected_commit

            version_tag_refs = None

            assert not at_commit if before_commit else not before_commit

            for tag_ref in repotools.git_get_tags_by_referred_object(context.repo, history_commit.obj_name):
                version_info = context.version_tag_matcher.to_version_info(tag_ref.name)
                if version_info is not None:
                    tag_matches = version_info.major == branch_base_version_info.major \
                                  and version_info.minor == branch_base_version_info.minor

                    if tag_matches:
                        if version_tag_refs is None:
                            version_tag_refs = list()
                        version_tag_refs.append(tag_ref)
                    else:
                        if fork_point is not None:
                            # fail stray tags on exclusive branch commits
                            result.fail(os.EX_DATAERR,
                                        _("Cannot bump version."),
                                        _("Found stray version tag: {version}.")
                                        .format(version=repr(version.format_version_info(version_info)))
                                        )
                        else:
                            # when no merge base is used, abort at the first mismatching tag
                            break

            if not abort_version_scan and version_tag_refs is not None and len(version_tag_refs):
                version_tag_refs.sort(
                    reverse=True,
                    key=utils.cmp_to_key(
                        lambda tag_ref_a, tag_ref_b: semver.compare(
                            context.version_tag_matcher.format(tag_ref_a.name),
                            context.version_tag_matcher.format(tag_ref_b.name)
                        )
                    )
                )
                if latest_version_tag is None:
                    latest_version_tag = version_tag_refs[0]
                if at_commit:
                    version_tags_on_same_commit.extend(version_tag_refs)
                if at_commit or before_commit:
                    if preceding_version_tag is None:
                        preceding_version_tag = version_tag_refs[0]
                    if on_selected_branch and preceding_branch_version_tag is None:
                        preceding_branch_version_tag = version_tag_refs[0]
                else:
                    subsequent_version_tags.extend(version_tag_refs)

                for tag_ref in version_tag_refs:
                    enclosing_versions.add(context.version_tag_matcher.format(tag_ref.name))

                if before_commit:
                    abort_version_scan = True

            if at_commit:
                before_commit = True

        if on_selected_branch:
            before_commit = True
            before_selected_branch = True

        if abort_version_scan:
            break

    if context.config.sequential_versioning and preceding_version_tag is not None:
        match = context.version_tag_matcher.fullmatch(preceding_version_tag.name)
        preceding_sequential_version = match.group(context.version_tag_matcher.group_unique_code)
    else:
        preceding_sequential_version = None
    if preceding_sequential_version is not None:
        preceding_sequential_version = int(preceding_sequential_version)

    if context.verbose:
        cli.print("Tags on selected commit:\n"
                  + '\n'.join(' - ' + repr(tag_ref.name) for tag_ref in version_tags_on_same_commit))

        cli.print("Tags in subsequent history:\n"
                  + '\n'.join(' - ' + repr(tag_ref.name) for tag_ref in subsequent_version_tags))

    if preceding_branch_version_tag is not None:
        latest_branch_version = context.version_tag_matcher.format(preceding_branch_version_tag.name)
    else:
        latest_branch_version = None

    global_sequence_number = get_global_sequence_number(context)
    if latest_branch_version is not None:
        version_result = operation(context.config.version_config, latest_branch_version, global_sequence_number)
        result.add_subresult(version_result)

        new_version = version_result.value
        if result.has_errors():
            return result
    else:
        template_version_info = semver.parse_version_info(context.config.version_config.initial_version)
        new_version = semver.format_version(
            major=selected_branch_base_version_info.major,
            minor=selected_branch_base_version_info.minor,

            patch=template_version_info.patch,
            prerelease=str(global_sequence_number + 1) if context.config.tie_sequential_version_to_semantic_version and global_sequence_number is not None else template_version_info.prerelease,
            build=template_version_info.build,
        )

    new_version_info = semver.parse_version_info(new_version)
    new_sequential_version = scheme_procedures.get_sequence_number(context.config.version_config, new_version_info)

    if new_version_info.major != selected_branch_base_version_info.major or new_version_info.minor != selected_branch_base_version_info.minor:
        result.fail(os.EX_USAGE,
                    _("Tag creation failed."),
                    _("The major.minor part of the new version {new_version}"
                      " does not match the branch version {branch_version}.")
                    .format(new_version=repr(new_version),
                            branch_version=repr(
                                "%d.%d" % (selected_branch_base_version_info.major, selected_branch_base_version_info.minor)))
                    )

    try:
        config_in_selected_commit = read_config_in_commit(context.repo, command_context.selected_commit)
    except FileNotFoundError:
        config_in_selected_commit = dict()

    try:
        properties_in_selected_commit = read_properties_in_commit(context,
                                                                  context.repo,
                                                                  config_in_selected_commit,
                                                                  command_context.selected_commit)
    except FileNotFoundError:
        properties_in_selected_commit = dict()

    if context.verbose:
        print("properties in selected commit:")
        print(json.dumps(obj=properties_in_selected_commit, indent=2))

    valid_tag = False

    # validate the commit
    if len(version_tags_on_same_commit):
        if config_in_selected_commit is None:
            result.fail(os.EX_DATAERR,
                        _("Tag creation failed."),
                        _("The selected commit does not contain a configuration file.")
                        )

        version_property_name = config_in_selected_commit.get(const.CONFIG_VERSION_PROPERTY)
        if version_property_name is not None \
                and properties_in_selected_commit.get(version_property_name) is None:
            result.warn(_("Missing version info."),
                        _("The selected commit does not contain a version in property '{property_name}'.")
                        .format(property_name=version_property_name)
                        )

    if len(version_tags_on_same_commit):
        if context.config.allow_qualifier_increments_within_commit:
            preceding_commit_version = context.version_tag_matcher.format(
                version_tags_on_same_commit[0].name)
            prerelease_keywords_list = [context.config.version_config.qualifiers, 1]

            preceding_commit_version_ = version.parse_version(preceding_commit_version)
            new_commit_version_ = version.parse_version(new_version)
            version_delta = version.determine_version_delta(preceding_commit_version_,
                                                            new_commit_version_,
                                                            prerelease_keywords_list
                                                            )

            version_increment_eval_result = version.evaluate_version_increment(preceding_commit_version_,
                                                                               new_commit_version_,
                                                                               context.config.strict_mode,
                                                                               prerelease_keywords_list)
            result.add_subresult(version_increment_eval_result)
            if result.has_errors():
                return result

            if not version_delta.prerelease_field_only(0, False):
                result.fail(os.EX_USAGE,
                            _("Tag creation failed."),
                            _("The selected commit already has version tags.\n"
                              "Operations on such a commit are limited to pre-release type increments.")
                            )

            valid_tag = True
        else:
            result.fail(os.EX_USAGE,
                        _("Tag creation failed."),
                        _("There are version tags pointing to the selected commit {commit}.\n"
                          "Consider reusing these versions or bumping them to stable."
                          "{listing}")
                        .format(commit=command_context.selected_commit,
                                listing='\n'.join(
                                    ' - ' + repr(tag_ref.name) for tag_ref in subsequent_version_tags))
                        )

    if not valid_tag:
        if len(subsequent_version_tags):
            result.fail(os.EX_USAGE,
                        _("Tag creation failed."),
                        _("There are version tags in branch history following the selected commit {commit}:\n"
                          "{listing}")
                        .format(commit=command_context.selected_commit,
                                listing='\n'.join(
                                    ' - ' + repr(tag_ref.name) for tag_ref in subsequent_version_tags))
                        )

    global_seq_number = global_sequence_number
    if context.config.tie_sequential_version_to_semantic_version \
            and global_seq_number is not None \
            and new_sequential_version is not None \
            and preceding_sequential_version != global_seq_number:
        result.fail(os.EX_USAGE,
                    _("Tag creation failed."),
                    _(
                        "The preceding sequential version {seq_val} "
                        "does not equal the global sequential version {global_seq_val}.")
                    .format(seq_val=preceding_sequential_version
                    if preceding_sequential_version is not None
                    else '<none>',
                            global_seq_val=global_seq_number)
                    )

    if not result.has_errors():
        if new_version is None:
            result.fail(os.EX_SOFTWARE,
                        _("Internal error."),
                        _("Missing result version.")
                        )
        if latest_branch_version is not None and semver.compare(latest_branch_version, new_version) >= 0:
            result.fail(os.EX_DATAERR,
                        _("Failed to increment version from {current} to {new}.")
                        .format(current=repr(latest_branch_version), new=repr(new_version)),
                        _("The new version is lower than or equal to the current version.")
                        )

        if context.config.push_to_local \
                and command_context.current_branch.short_name == command_context.selected_ref.short_name:
            if context.verbose:
                cli.print(
                    _('Checking out {base_branch} in order to avoid failing the push to a checked-out release branch')
                        .format(base_branch=repr(context.config.release_branch_base)))

            git_or_fail(context.repo, result, ['checkout', context.config.release_branch_base])
            original_current_branch = command_context.current_branch
        else:
            original_current_branch = None

        branch_name = get_branch_name_for_version(context, new_version_info)
        tag_name = get_tag_name_for_version(context, new_version_info)

        clone_result = clone_repository(context, context.config.release_branch_base)
        cloned_repo = clone_result.value

        commit_info = CommitInfo()
        commit_info.add_message("#version: " + cli.if_none(new_version))

        # run version change hooks on release branch
        checkout_command = ['checkout', '--force', '--track', '-b', branch_name,
                            repotools.create_ref_name(const.REMOTES_PREFIX,
                                                      context.config.remote_name,
                                                      branch_name)]
        returncode, out, err = repotools.git(cloned_repo, *checkout_command)
        if returncode != os.EX_OK:
            result.fail(os.EX_DATAERR,
                        _("Failed to check out release branch."),
                        _("An unexpected error occurred.")
                        )

        clone_context: Context = create_temp_context(context, result, cloned_repo.dir)
        clone_context.config.remote_name = 'origin'

        if (context.config.commit_version_property and new_version is not None) \
                or (context.config.commit_sequential_version_property and new_sequential_version is not None):

            update_result = update_project_property_file(clone_context,
                                                         properties_in_selected_commit,
                                                         new_version,
                                                         new_sequential_version,
                                                         commit_info)
            result.add_subresult(update_result)
            if result.has_errors():
                result.fail(os.EX_DATAERR,
                            _("Property update failed."),
                            _("An unexpected error occurred.")
                            )

        if new_version is not None:
            execute_version_change_actions(clone_context, latest_branch_version, new_version)

        if commit_info is not None:
            if command_context.selected_commit != command_context.selected_ref.target.obj_name:
                result.fail(os.EX_USAGE,
                            _("Failed to commit version update."),
                            _("The selected parent commit {commit} does not represent the tip of {branch}.")
                            .format(commit=command_context.selected_commit,
                                    branch=repr(command_context.selected_ref.name))
                            )

            # commit changes
            commit_info.add_parent(command_context.selected_commit)
            object_to_tag = create_commit(clone_context, result, commit_info)
            new_branch_ref_object = object_to_tag
        else:
            object_to_tag = command_context.selected_commit
            new_branch_ref_object = None

        # if command_context.selected_branch not in repotools.git_list_refs(context.repo,
        #                                                                   '--contains', object_to_tag,
        #                                                                   command_context.selected_branch.ref):

        # show info and prompt for confirmation
        cli.print("ref                 : " + cli.if_none(command_context.selected_ref.name))
        cli.print("ref_" + const.DEFAULT_VERSION_VAR_NAME + "         : " + cli.if_none(latest_branch_version))
        cli.print("new_tag             : " + cli.if_none(tag_name))
        cli.print("new_" + const.DEFAULT_VERSION_VAR_NAME + "         : " + cli.if_none(new_version))
        cli.print("selected object     : " + cli.if_none(command_context.selected_commit))
        cli.print("tagged object       : " + cli.if_none(object_to_tag))

        prompt_result = prompt_for_confirmation(
            context=context,
            fail_title=_("Failed to create release tag based on {branch}.")
                .format(branch=repr(command_context.selected_ref.name)),
            message=_("The tags are about to be pushed."),
            prompt=_("Continue?"),
        )
        result.add_subresult(prompt_result)
        if result.has_errors() or not prompt_result.value:
            return result

        # push atomically
        push_command = ['push', '--atomic']
        if context.dry_run:
            push_command.append('--dry-run')
        if context.verbose:
            push_command.append('--verbose')
        push_command.append(context.config.remote_name)

        # push the release branch commit or its version increment commit
        if new_branch_ref_object is not None:
            push_command.append(
                new_branch_ref_object + ':' + repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, branch_name))

        # check, if preceding tags exist on remote
        if preceding_version_tag is not None:
            push_command.append('--force-with-lease='
                                + preceding_version_tag.name + ':'
                                + preceding_version_tag.name)

        # push the new version tag or fail if it exists
        push_command.extend(['--force-with-lease=' + repotools.create_ref_name(const.LOCAL_TAG_PREFIX, tag_name) + ':',
                             repotools.ref_target(object_to_tag) + ':' + repotools.create_ref_name(
                                 const.LOCAL_TAG_PREFIX, tag_name)])

        returncode, out, err = repotools.git(clone_context.repo, *push_command)
        if returncode != os.EX_OK:
            result.fail(os.EX_DATAERR,
                        _("Failed to push."),
                        _("git push exited with " + str(returncode))
                        )

        if original_current_branch is not None:
            if context.verbose:
                cli.print(
                    _('Switching back to {original_branch} ')
                        .format(original_branch=repr(original_current_branch.name)))

            git_or_fail(context.repo, result, ['checkout', original_current_branch.short_name])

    return result