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
def select_ref(result_out: Result, branch_info: BranchInfo, selection: BranchSelection) \ -> [repotools.Ref, const.BranchClass]: if branch_info.local is not None and len( branch_info.local) and branch_info.upstream is not None: if branch_info.local_class[0] != branch_info.upstream_class: result_out.error( os.EX_DATAERR, _("Local and upstream branch have a mismatching branch class." ), None) if not branch_info.upstream.short_name.endswith( '/' + branch_info.local[0].short_name): result_out.error( os.EX_DATAERR, _("Local and upstream branch have a mismatching short name."), None) candidate = None candidate_class = None if selection == BranchSelection.BRANCH_PREFER_LOCAL: candidate = branch_info.local[0] or branch_info.upstream candidate_class = branch_info.local_class[ 0] or branch_info.upstream_class elif selection == BranchSelection.BRANCH_LOCAL_ONLY: candidate = branch_info.local[0] candidate_class = branch_info.local_class[0] elif selection == BranchSelection.BRANCH_PREFER_REMOTE: candidate = branch_info.upstream or branch_info.local[0] candidate_class = branch_info.upstream_class or branch_info.local_class[ 0] elif selection == BranchSelection.BRANCH_REMOTE_ONLY: candidate = branch_info.upstream candidate_class = branch_info.upstream_class return candidate, candidate_class
def evaluate_numeric_increment(result: Result, field_name: str, reset: bool, reset_val: int, strict: bool, a: int, b: int): """ :rtype: bool """ delta = b - a if reset: if b != reset_val: result.error(os.EX_USAGE, _("Version change leaves a gap without a semantic meaning."), _("The field {field_name} must be reset to {reset_val}.") .format(field_name=repr(field_name) , reset_val=reset_val) ) else: if delta > 1: result.error(os.EX_USAGE if strict else os.EX_OK, _("Version change leaves a gap without a semantic meaning."), _("The field {field_name} must not be incremented by more than one.") .format(field_name=repr(field_name)) ) return reset or b > a
def evaluate_prerelease_increment(result: Result, field_name: str, index: int, reset: bool, reset_val: int or str, strict: bool, a: int or str, b: int or str, keywords: list): """ :rtype: bool """ delta = cmp_alnum_token(b, a, keywords) requires_reset = False if delta > 0: requires_reset = True if reset: if b != reset_val: result.error(os.EX_USAGE if strict else os.EX_OK, _("Version change leaves a gap without a semantic meaning."), _("The field {field_name} must be reset to {reset_val}.") .format(field_name=repr(field_name + '[' + str(index) + ']'), reset_val=reset_val) ) else: if delta > 1: result.error(os.EX_USAGE if strict else os.EX_OK, _("Version change leaves a gap without a semantic meaning."), _("The field {field_name} must not be incremented by more than one.") .format(field_name=repr(field_name + '[' + str(index) + ']')) ) return reset or requires_reset
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()
def version_bump_to_release(version_config: VersionConfig, version: Optional[str], global_seq: Optional[int]): result = Result() version_info = semver.parse_version_info( version) if version is not None else semver.parse_version_info("0.0.0") if version_config.versioning_scheme == VersioningScheme.SEMVER_WITH_SEQ: result.error( os.EX_USAGE, _("Failed to increment version to release: {version}.").format( version=repr(version)), _("Sequential versions cannot be release versions.")) return result if not version_info.prerelease: result.error( os.EX_DATAERR, _("Failed to increment version to release: {version}.").format( version=repr(version)), _("Only pre-release versions can be incremented to a release version." )) if not result.has_errors(): result.value = semver.format_version(version_info.major, version_info.minor, version_info.patch, None, None) return result
def fail(exit_code, *message): # TODO remove for line in message: eprint(line) if exit_code == os.EX_OK: eprint("internal error") exit_code = os.EX_SOFTWARE result = Result() result.error(exit_code, os.linesep.join(message), None) raise GitFlowException(result)
def version_bump_prerelease(version_config: VersionConfig, version: Optional[str], global_seq: Optional[int]): result = Result() version_info = semver.parse_version_info( version) if version is not None else semver.parse_version_info("0.0.0") if version_info.prerelease: prerelease_version_elements = version_info.prerelease.split(".") if len(prerelease_version_elements ) > 0 and prerelease_version_elements[0].upper() == "SNAPSHOT": if len(prerelease_version_elements) == 1: result.error( os.EX_DATAERR, _("The pre-release increment has been skipped."), _("In order to retain Maven compatibility, " "the pre-release component of snapshot versions must not be versioned." )) else: result.error( os.EX_DATAERR, _("Failed to increment the pre-release component of version {version}." ).format(version=repr(version)), _("Snapshot versions must not have a pre-release version.") ) result.value = version elif len(prerelease_version_elements) == 1: if version_config.versioning_scheme != VersioningScheme.SEMVER_WITH_SEQ: result.error( os.EX_DATAERR, _("Failed to increment the pre-release component of version {version}." ).format(version=repr(version)), _("The qualifier {qualifier} must already be versioned."). format(qualifier=repr(prerelease_version_elements[0]))) result.value = semver.bump_prerelease(version) else: result.error( os.EX_DATAERR, _("Failed to increment the pre-release component of version {version}." ).format(version=repr(version)), _("Pre-release increments cannot be performed on release versions." )) if result.has_errors(): result.value = None elif result.value is not None and not semver.compare( result.value, version) > 0: result.value = None if not result.value: result.error( os.EX_SOFTWARE, _("Failed to increment the pre-release of version {version} for unknown reasons." ).format(version=repr(version)), None) return result
def evaluate_version_increment(a: Version, b: Version, strict: bool, prerelase_keywords_list: list = None): result = Result() initial_version = Version() initial_version.major = 1 initial_version.minor = 0 initial_version.patch = 0 if prerelase_keywords_list is not None and len(prerelase_keywords_list): initial_version.prerelease = list() for index, token_config in enumerate(prerelase_keywords_list): if token_config is not None: if isinstance(token_config, list) and len(token_config): initial_version.prerelease.append(token_config[0]) elif isinstance(token_config, int): initial_version.prerelease.append(token_config) else: raise ValueError() else: initial_version.prerelease.append(0) reset = False reset = evaluate_numeric_increment(result, 'major', reset, initial_version.major, strict, a.major, b.major) reset = evaluate_numeric_increment(result, 'minor', reset, initial_version.minor, strict, a.minor, b.minor) reset = evaluate_numeric_increment(result, 'patch', reset, initial_version.patch, strict, a.patch, b.patch) # check pre-release convention index = 0 for sub_a, sub_b in itertools.zip_longest(a.prerelease or [], b.prerelease or []): keywords = prerelase_keywords_list[index] \ if prerelase_keywords_list is not None \ and index < len(prerelase_keywords_list) \ else None reset = evaluate_prerelease_increment(result, "prerelease", index, reset, initial_version.prerelease[index] if initial_version.prerelease is not None and index < len(initial_version.prerelease) else 0, strict, sub_a, sub_b, keywords) index += 1 if result.has_errors(): result.error(os.EX_USAGE if strict else os.EX_OK, _("Version increment is flawed."), _("A version increment from {version_a} to {version_b} is inconsistent.") .format(version_a=repr(format_version(a)), version_b=repr(format_version(b))) ) return result
def version_bump_qualifier(version_config: VersionConfig, version: Optional[str], global_seq: Optional[int]): result = Result() version_info = semver.parse_version_info( version) if version is not None else semver.parse_version_info("0.0.0") new_qualifier = None if not version_config.qualifiers: result.error( os.EX_USAGE, _("Failed to increment the pre-release qualifier of version {version}." ).format(version=repr(version)), _("The version scheme does not contain qualifiers")) return result if version_info.prerelease: prerelease_version_elements = version_info.prerelease.split(".") qualifier = prerelease_version_elements[0] qualifier_index = version_config.qualifiers.index( qualifier) if qualifier in version_config.qualifiers else -1 if qualifier_index < 0: result.error( os.EX_DATAERR, _("Failed to increment the pre-release qualifier of version {version}." ).format(version=repr(version)), _("The current qualifier is invalid: {qualifier}").format( qualifier=repr(qualifier))) else: qualifier_index += 1 if qualifier_index < len(version_config.qualifiers): new_qualifier = version_config.qualifiers[qualifier_index] else: result.error( os.EX_DATAERR, _("Failed to increment the pre-release qualifier {qualifier} of version {version}." ).format(qualifier=qualifier, version=repr(version)), _("There are no further qualifiers with higher precedence, configured qualifiers are:\n" "{listing}\n" "The sub command 'bump-to-release' may be used for a final bump." ).format(listing='\n'.join( ' - ' + repr(qualifier) for qualifier in version_config.qualifiers))) else: result.error( os.EX_DATAERR, _("Failed to increment the pre-release qualifier of version {version}." ).format(version=version), _("Pre-release increments cannot be performed on release versions." )) if not result.has_errors() and new_qualifier is not None: result.value = semver.format_version(version_info.major, version_info.minor, version_info.patch, new_qualifier + ".1", None) return result
def download_file(source_uri: str, dest_file: str, hash_hex: str): from urllib import request import hashlib result = Result() hash = bytes.fromhex(hash_hex) download = False if not os.path.exists(dest_file): cli.print("file does not exist: " + dest_file) download = True elif hash_file(hashlib.sha256(), dest_file) != hash: cli.print("file hash does not match: " + dest_file) download = True else: cli.print("keeping file: " + dest_file + ", sha256 matched: " + hash_hex) if download: cli.print("downloading: " + source_uri + " to " + dest_file) request.urlretrieve(url=str(source_uri), filename=dest_file + "~") filesystem.replace_file(dest_file + "~", dest_file) if hash is not None: actual_hash = hash_file(hashlib.sha256(), dest_file) if actual_hash != hash: result.error( os.EX_IOERR, _("File verification failed."), _("The file {file} is expected to hash to {expected_hash},\n" "The actual hash is: {actual_hash}").format( file=repr(dest_file), expected_hash=repr(hash_hex), actual_hash=repr(actual_hash.hex()), )) if not result.has_errors(): result.value = dest_file return result
def validate_version(config: VersionConfig, version_string): result = Result() try: version_info = semver.parse_version_info(version_string) if version_info.prerelease is not None: if version_info.prerelease is not None and not re.match(r'[a-zA-Z][a-zA-Z0-9]*\.\d', version_info.prerelease): result.error(os.EX_DATAERR, "Invalid version format.", "The pre-release component must contain a type name with a version number.\n" "The required version format is:\n" + const.TEXT_VERSION_STRING_FORMAT) prerelease_version_elements = version_info.prerelease.split(".") prerelease_type = prerelease_version_elements[0] prerelease_version = prerelease_version_elements[1] if config.qualifiers is not None and prerelease_type not in config.qualifiers: result.error(os.EX_DATAERR, "Invalid version.", "The pre-release type \"" + prerelease_type + "\" is invalid, must be one of: " + ','.join(config.qualifiers) + ".\n" + "Configuration property: " + const.CONFIG_VERSION_TYPES) result.value = version_string except ValueError: result.error(os.EX_DATAERR, "Failed to parse the version.", "The required version format is:\n" + const.TEXT_VERSION_STRING_FORMAT) return result
def version_bump_patch(version_config: VersionConfig, version: Optional[str], global_seq: Optional[int]): result = Result() try: global_seq = filter_sequence_number(version_config, version, global_seq) except ValueError as e: result.error(os.EX_DATAERR, "version increment failed", str(e)) if not result.has_errors(): version_info = semver.parse_version_info( semver.bump_patch(version) ) if version is not None else semver.parse_version_info("0.0.0") pre_release = True result.value = semver.format_version( version_info.major, version_info.minor, version_info.patch, (version_config.qualifiers[0] + ".1" if pre_release else None) if version_config.versioning_scheme != VersioningScheme.SEMVER_WITH_SEQ else global_seq + 1, None) return result
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
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