def cmd_drop_cache(context): result = Result() cache_root = filesystem.get_cache_root_dir() cli.print("dropping cache root: " + repr(cache_root)) if not context.dry_run: filesystem.delete_all_cache_dirs() return result
def pre_push(context: Context) -> Result: result = Result() for line in sys.stdin.readlines(): tokens = line.split(' ') if len(tokens) != 4: raise ValueError() cli.print(line) local_ref = tokens[0] local_sha1 = tokens[1] remote_ref = tokens[2] remote_sha1 = tokens[3] command_context = common.get_command_context(context=context, object_arg=remote_ref) common.check_requirements(command_context=command_context, ref=command_context.selected_ref, branch_classes=None, modifiable=True, with_upstream=False, in_sync_with_upstream=False, fail_message=_("Push rejected."), throw=False) return result
def cleanup(self): atexit.unregister(self.cleanup) if self.temp_dirs is not None: for temp_dir in self.temp_dirs: if self.verbose >= const.DEBUG_VERBOSITY: cli.print("deleting temp dir: " + temp_dir) shutil.rmtree(temp_dir) self.temp_dirs.clear() if self.clones is not None: for clone in self.clones: clone.cleanup() self.clones.clear()
def execute_build_steps(command_context: CommandContext, types: list = None): if types is not None: stages = filter(lambda stage: stage.type in types, command_context.context.config.build_stages) else: stages = command_context.context.config.build_stages for stage in stages: for step in stage.steps: step_errors = 0 for command in step.commands: command_string = ' '.join( shlex.quote(token) for token in command) if command_context.context.verbose >= const.TRACE_VERBOSITY: print(command_string) command = [expand_vars(token, os.environ) for token in command] if not command_context.context.dry_run: try: proc = subprocess.Popen( args=command, stdin=subprocess.PIPE, cwd=command_context.context.root) proc.wait() if proc.returncode != os.EX_OK: command_context.fail( os.EX_DATAERR, _("{stage}:{step} failed.").format( stage=stage.name, step=step.name), _("{command}\n" "returned with an error.").format( command=command_string)) except FileNotFoundError as e: step_errors += 1 command_context.fail( os.EX_DATAERR, _("{stage}:{step} failed.").format( stage=stage.name, step=step.name), _("{command}\n" "could not be executed.\n" "File not found: {file}").format( command=command_string, file=e.filename)) if not step_errors: cli.print(stage.name + ":" + step.name + ": OK") else: cli.print(stage.name + ":" + step.name + ": FAILED")
def git_interactive(context: RepoContext, *args) -> subprocess.Popen: command = [context.git] if context.use_root_dir_arg: command.extend(['-C', context.dir]) command.extend(args) for index, arg in enumerate(command): if isinstance(arg, Ref): command[index] = arg.name if context.verbose >= const.TRACE_VERBOSITY: cli.print(' '.join(shlex.quote(token) for token in command)) return subprocess.Popen( args=command, cwd=context.dir if not context.use_root_dir_arg else None)
def git_raw(git: str, args: list, verbose: int, dir: str = None) -> typing.Tuple[int, bytes, bytes]: command = [git] if dir is not None: command.extend(['-C', dir]) for index, arg in enumerate(args): if isinstance(arg, Ref): args[index] = arg.name command.extend(args) if verbose >= const.TRACE_VERBOSITY: cli.print(utils.command_to_str(command)) env = os.environ.copy() env["LANGUAGE"] = "C" env["LC_ALL"] = "C" if verbose >= const.TRACE_VERBOSITY: proc = subprocess.Popen(args=command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=dir, env=env) else: proc = subprocess.Popen(args=command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dir, env=env) out, err = proc.communicate() if proc.returncode != os.EX_OK: if verbose >= const.TRACE_VERBOSITY: cli.eprint("command failed: " + utils.command_to_str(command)) cli.eprint("child process returned " + str(proc.returncode)) if err is not None: cli.eprint(err.decode("utf-8")) return proc.returncode, out, err
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 get_command_context(context, object_arg: str) -> CommandContext: command_context = CommandContext() command_context.object_arg = object_arg command_context.context = context if context.repo is not None: command_context.upstreams = repotools.git_get_upstreams( context.repo, const.LOCAL_BRANCH_PREFIX) command_context.downstreams = { v: k for k, v in command_context.upstreams.items() } # resolve the full rev name and its hash for consistency selected_ref = None current_branch = repotools.git_get_current_branch(context.repo) affected_main_branches = None if object_arg is None: if current_branch is None: command_context.fail( os.EX_USAGE, _("Operation failed."), _("No object specified and not on a branch (may be an empty repository)." )) commit = current_branch.target.obj_name selected_ref = current_branch else: branch_ref = get_branch_by_branch_name_or_version_tag( context, object_arg, BranchSelection.BRANCH_PREFER_LOCAL) if branch_ref is not None: selected_ref = branch_ref commit = branch_ref.target.obj_name else: branch_ref = repotools.git_rev_parse(context.repo, '--revs-only', '--symbolic-full-name', object_arg) commit = repotools.git_rev_parse(context.repo, '--revs-only', object_arg) if branch_ref is not None: selected_ref = repotools.Ref() selected_ref.name = branch_ref selected_ref.obj_type = 'commit' selected_ref.obj_name = commit if commit is None: command_context.fail( os.EX_USAGE, _("Failed to resolve object {object}.").format( object=repr(object_arg)), _("No corresponding commit found.")) # determine affected branches affected_main_branches = list( filter( lambda ref: (ref.name not in command_context.downstreams and commit in [ reachable_commit.obj_name for reachable_commit in repotools .git_list_commits(context=context.repo, start=None, end=ref, options=['--first-parent']) ]), repotools.git_list_refs( context.repo, '--contains', commit, repotools.create_ref_name(const.REMOTES_PREFIX, context.config.remote_name, 'release'), 'refs/heads/release', 'refs/heads/' + context.config.release_branch_base))) if len(affected_main_branches) == 1: if selected_ref is None or selected_ref.name.startswith( const.LOCAL_TAG_PREFIX): selected_ref = affected_main_branches[0] if selected_ref is None: if len(affected_main_branches) == 0: command_context.fail( os.EX_USAGE, _("Failed to resolve target branch"), _("Failed to resolve branch containing object: {object}"). format(object=repr(object_arg))) else: command_context.fail( os.EX_USAGE, _("Failed to resolve unique branch for object: {object}"). format(object=repr(object_arg)), _("Multiple different branches contain this commit:\n" "{listing}").format(listing='\n'.join( ' - ' + repr(ref.name) for ref in affected_main_branches))) if selected_ref is None or commit is None: command_context.fail( os.EX_USAGE, _("Failed to resolve ref."), _("{object} could not be resolved.").format( object=repr(object_arg))) if context.verbose >= const.INFO_VERBOSITY: cli.print( _("Target branch: {name} ({commit})").format( name=repr(selected_ref.name), commit=selected_ref.target.obj_name)) cli.print(_("Target commit: {commit}").format(commit=commit)) branch_info = get_branch_info(command_context, selected_ref) command_context.selected_ref = selected_ref command_context.selected_commit = commit command_context.selected_branch = branch_info command_context.selected_explicitly = object_arg is not None command_context.affected_main_branches = affected_main_branches command_context.current_branch = current_branch return command_context
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
def call(context: Context) -> Result: command_context = get_command_context( context=context, object_arg=context.args['<base-object>']) check_in_repo(command_context) check_requirements( command_context=command_context, ref=command_context.selected_ref, branch_classes=None, modifiable=True, with_upstream=True, # not context.config.push_to_local in_sync_with_upstream=True, fail_message=_("Version creation failed.")) selected_work_branch = context.args.get('<work-branch>') if selected_work_branch is not None: selected_work_branch = repotools.create_ref_name(selected_work_branch) if not selected_work_branch.startswith(const.LOCAL_BRANCH_PREFIX): selected_work_branch = const.LOCAL_BRANCH_PREFIX + selected_work_branch branch_match = context.work_branch_matcher.fullmatch( selected_work_branch) if branch_match is None: context.fail( os.EX_USAGE, _("Invalid work branch: {branch}.").format( branch=repr(selected_work_branch)), None) groups = branch_match.groupdict() branch_supertype = groups['prefix'] branch_type = groups['type'] branch_short_name = groups['name'] else: branch_supertype = context.args['<supertype>'] branch_type = context.args['<type>'] branch_short_name = context.args['<name>'] if branch_supertype not in [ const.BRANCH_PREFIX_DEV, const.BRANCH_PREFIX_PROD ]: context.fail( os.EX_USAGE, _("Invalid branch super type: {supertype}.").format( supertype=repr(branch_supertype)), None) work_branch_name = repotools.create_ref_name(branch_supertype, branch_type, branch_short_name) work_branch_ref_name = repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, work_branch_name) work_branch_class = get_branch_class(context, work_branch_ref_name) if True: work_branch_info = get_branch_info(command_context, work_branch_ref_name) if work_branch_info is not None: context.fail( os.EX_USAGE, _("The branch {branch} already exists locally or remotely."). format(branch=repr(work_branch_name)), None) allowed_base_branch_class = const.BRANCHING[work_branch_class] base_branch, base_branch_class = select_ref( command_context.result, command_context.selected_branch, BranchSelection.BRANCH_PREFER_LOCAL) if not command_context.selected_explicitly and branch_supertype == const.BRANCH_PREFIX_DEV: base_branch_info = get_branch_info( command_context, repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, context.config.release_branch_base)) base_branch, base_branch_class = select_ref( command_context.result, base_branch_info, BranchSelection.BRANCH_PREFER_LOCAL) if allowed_base_branch_class != base_branch_class: context.fail( os.EX_USAGE, _("The branch {branch} is not a valid base for {supertype} branches." ).format(branch=repr(base_branch.name), supertype=repr(branch_supertype)), None) if base_branch is None: context.fail(os.EX_USAGE, _("Base branch undetermined."), None) if context.verbose: cli.print("branch_name: " + command_context.selected_ref.name) cli.print("work_branch_name: " + work_branch_name) cli.print("base_branch_name: " + base_branch.name) if not context.dry_run and not command_context.has_errors(): index_status = git(context.repo, ['diff-index', 'HEAD', '--']) if index_status == 1: context.fail( os.EX_USAGE, _("Branch creation aborted."), _("You have staged changes in your workspace.\n" "Unstage, commit or stash them and try again.")) elif index_status != 0: context.fail(os.EX_DATAERR, _("Failed to determine index status."), None) git_or_fail( context.repo, command_context.result, [ 'update-ref', work_branch_ref_name, command_context.selected_commit, '' ], _("Failed to create branch {branch_name}.").format( branch_name=work_branch_name)) git_or_fail( context.repo, command_context.result, ['checkout', work_branch_name], _("Failed to checkout branch {branch_name}.").format( branch_name=work_branch_name)) return context.result
def main(argv: list = sys.argv) -> int: if ENABLE_PROFILER: import cProfile profiler = cProfile.Profile() profiler.enable() else: profiler = None result = Result() args = docopt.docopt(argv=argv[1:], doc=__doc__, version=const.VERSION, help=True, options_first=False) try: context = Context.create(args, result) except GitFlowException as e: context = None pass # errors are in result if context is not None: try: if context.verbose >= const.DEBUG_VERBOSITY: cli.print("GitFlow version: " + const.VERSION) cli.print("Python version:" + sys.version.replace('\n', ' ')) cli.print("cwd: " + os.getcwd()) if args['--hook'] is not None: if context.verbose >= const.TRACE_VERBOSITY: cli.print('hook=' + args['--hook']) hook_func = cli.get_cmd([ hook_pre_commit, hook_pre_push, ], args['--hook'], 'hook_') try: hook_result = hook_func(context) except GitFlowException as e: hook_result = e.result result.errors.extend(hook_result.errors) else: commands = { 'status': cmd_status, 'bump-major': cmd_bump_major, 'bump-minor': cmd_bump_minor, 'bump-patch': cmd_bump_patch, 'bump-prerelease-type': cmd_bump_prerelease_type, 'bump-prerelease': cmd_bump_prerelease, 'bump-to-release': cmd_bump_to_release, 'bump-to': cmd_bump_to, 'discontinue': cmd_discontinue, 'start': cmd_start, 'finish': cmd_finish, 'log': cmd_log, 'assemble': cmd_build, 'test': cmd_build, 'integration-test': cmd_build, 'drop-cache': cmd_drop_cache, 'convert-config': cmd_convert_config, } command_funcs = list() for command_name, command_func in commands.items(): if args[command_name] is True: command_funcs.append(command_func) if not len(command_funcs): cli.fail(os.EX_SOFTWARE, "unimplemented command") if context.verbose >= const.TRACE_VERBOSITY: cli.print("commands: " + repr(command_funcs)) start_branch = repotools.git_get_current_branch(context.repo) if context.repo is not None else None for command_func in command_funcs: try: command_result = command_func(context) except GitFlowException as e: command_result = e.result result.errors.extend(command_result.errors) if result.has_errors(): break current_branch = repotools.git_get_current_branch(context.repo) if context.repo is not None else None if current_branch is not None and current_branch != start_branch: cli.print(_("You are now on {branch}.") .format(branch=repr(current_branch.short_name) if current_branch is not None else '-')) finally: context.cleanup() exit_code = os.EX_OK if len(result.errors): sys.stderr.flush() sys.stdout.flush() for error in result.errors: if error.exit_code != os.EX_OK and exit_code != os.EX_SOFTWARE: exit_code = error.exit_code cli.eprint('\n'.join(filter(None, [error.message, error.reason]))) # print dry run status, if possible if context is not None: if exit_code == os.EX_OK: if context.dry_run: cli.print('') cli.print("dry run succeeded") else: pass else: if context.dry_run: cli.print('') cli.eprint("dry run failed") else: pass if profiler is not None: profiler.disable() # pr.dump_stats('profile.pstat') profiler.print_stats(sort="calls") return exit_code
def call(context: Context) -> Result: arg_work_branch = context.args.get('<work-branch>') if arg_work_branch is None: branch_prefix = context.args['<supertype>'] branch_type = context.args['<type>'] branch_name = context.args['<name>'] if branch_prefix is not None or branch_type is not None or branch_name is not None: arg_work_branch = repotools.create_ref_name(branch_prefix, branch_type, branch_name) command_context = get_command_context( context=context, object_arg=arg_work_branch ) check_in_repo(command_context) base_command_context = get_command_context( context=context, object_arg=context.args['<base-object>'] ) check_requirements(command_context=command_context, ref=command_context.selected_ref, branch_classes=[BranchClass.WORK_DEV, BranchClass.WORK_PROD], modifiable=True, with_upstream=True, # not context.config.push_to_local in_sync_with_upstream=True, fail_message=_("Version creation failed.") ) work_branch = None selected_ref_match = context.work_branch_matcher.fullmatch(command_context.selected_ref.name) if selected_ref_match is not None: work_branch = WorkBranch() work_branch.prefix = selected_ref_match.group('prefix') work_branch.type = selected_ref_match.group('type') work_branch.name = selected_ref_match.group('name') else: if command_context.selected_explicitly: context.fail(os.EX_USAGE, _("The ref {branch} does not refer to a work branch.") .format(branch=repr(command_context.selected_ref.name)), None) work_branch_info = get_branch_info(command_context, work_branch.local_ref_name()) if work_branch_info is None: context.fail(os.EX_USAGE, _("The branch {branch} does neither exist locally nor remotely.") .format(branch=repr(work_branch.branch_name())), None) work_branch_ref, work_branch_class = select_ref(command_context.result, work_branch_info, BranchSelection.BRANCH_PREFER_LOCAL) allowed_base_branch_class = const.BRANCHING[work_branch_class] base_branch_info = get_branch_info(base_command_context, base_command_context.selected_ref) base_branch_ref, base_branch_class = select_ref(command_context.result, base_branch_info, BranchSelection.BRANCH_PREFER_LOCAL) if not base_command_context.selected_explicitly: if work_branch.prefix == const.BRANCH_PREFIX_DEV: base_branch_info = get_branch_info(base_command_context, repotools.create_ref_name(const.LOCAL_BRANCH_PREFIX, context.config.release_branch_base)) base_branch_ref, base_branch_class = select_ref(command_context.result, base_branch_info, BranchSelection.BRANCH_PREFER_LOCAL) elif work_branch.prefix == const.BRANCH_PREFIX_PROD: # discover closest merge base in release branches release_branches = repotools.git_list_refs(context.repo, repotools.create_ref_name(const.REMOTES_PREFIX, context.config.remote_name, 'release')) release_branches = list(release_branches) release_branches.sort(reverse=True, key=utils.cmp_to_key(lambda ref_a, ref_b: semver.compare( context.release_branch_matcher.format(ref_a.name), context.release_branch_matcher.format(ref_b.name) ))) for release_branch_ref in release_branches: merge_base = repotools.git_merge_base(context.repo, base_branch_ref, work_branch_ref.name) if merge_base is not None: base_branch_info = get_branch_info(base_command_context, release_branch_ref) base_branch_ref, base_branch_class = select_ref(command_context.result, base_branch_info, BranchSelection.BRANCH_PREFER_LOCAL) break if allowed_base_branch_class != base_branch_class: context.fail(os.EX_USAGE, _("The branch {branch} is not a valid base for {supertype} branches.") .format(branch=repr(base_branch_ref.name), supertype=repr(work_branch.prefix)), None) if base_branch_ref is None: context.fail(os.EX_USAGE, _("Base branch undetermined."), None) if context.verbose: cli.print("branch_name: " + command_context.selected_ref.name) cli.print("work_branch_name: " + work_branch_ref.name) cli.print("base_branch_name: " + base_branch_ref.name) # check, if already merged merge_base = repotools.git_merge_base(context.repo, base_branch_ref, work_branch_ref.name) if work_branch_ref.obj_name == merge_base: cli.print(_("Branch {branch} is already merged.") .format(branch=repr(work_branch_ref.name))) return context.result # check for staged changes index_status = git(context.repo, ['diff-index', 'HEAD', '--']) if index_status == 1: context.fail(os.EX_USAGE, _("Branch creation aborted."), _("You have staged changes in your workspace.\n" "Unstage, commit or stash them and try again.")) elif index_status != 0: context.fail(os.EX_DATAERR, _("Failed to determine index status."), None) if not context.dry_run and not command_context.has_errors(): # perform merge local_branch_ref_name = repotools.create_local_branch_ref_name(base_branch_ref.name) local_branch_name = repotools.create_local_branch_name(base_branch_ref.name) if local_branch_ref_name == base_branch_ref.name: git_or_fail(context.repo, command_context.result, ['checkout', local_branch_name], _("Failed to checkout branch {branch_name}.") .format(branch_name=repr(base_branch_ref.short_name)) ) else: git_or_fail(context.repo, command_context.result, ['checkout', '-b', local_branch_name, base_branch_ref.name], _("Failed to checkout branch {branch_name}.") .format(branch_name=repr(base_branch_ref.short_name)) ) git_or_fail(context.repo, command_context.result, ['merge', '--no-ff', work_branch_ref], _("Failed to merge work branch.\n" "Rebase {work_branch} on {base_branch} and try again") .format(work_branch=repr(work_branch_ref.short_name), base_branch=repr(base_branch_ref.short_name)) ) git_or_fail(context.repo, command_context.result, ['push', context.config.remote_name, local_branch_name], _("Failed to push branch {branch_name}.") .format(branch_name=repr(base_branch_ref.short_name)) ) return context.result
def call(context: Context) -> Result: result: Result = context.result object_arg = context.args['<object>'] reintegrate = cli.get_boolean_opt(context.args, '--reintegrate') command_context = get_command_context(context=context, object_arg=context.args['<object>']) check_in_repo(command_context) base_branch_ref = repotools.get_branch_by_name( context.repo, {context.config.remote_name}, context.config.release_branch_base, BranchSelection.BRANCH_PREFER_LOCAL) release_branch = command_context.selected_ref release_branch_info = get_branch_info(command_context, release_branch) check_requirements( command_context=command_context, ref=release_branch, branch_classes=[BranchClass.RELEASE], modifiable=True, with_upstream=True, # not context.config.push_to_local in_sync_with_upstream=True, fail_message=_("Build failed.")) if release_branch is None: command_context.fail( os.EX_USAGE, _("Branch discontinuation failed."), _("Failed to resolve an object for token {object}.").format( object=repr(object_arg))) discontinuation_tags, discontinuation_tag_name = get_discontinuation_tags( context, release_branch) if discontinuation_tag_name is None: command_context.fail( os.EX_USAGE, _("Branch discontinuation failed."), _("{branch} cannot be discontinued.").format( branch=repr(release_branch.name))) if context.verbose: cli.print("discontinuation tags:") for discontinuation_tag in discontinuation_tags: print(' - ' + discontinuation_tag.name) pass if len(discontinuation_tags): command_context.fail( os.EX_USAGE, _("Branch discontinuation failed."), _("The branch {branch} is already discontinued.").format( branch=repr(release_branch.name))) # show info and prompt for confirmation print("discontinued_branch : " + cli.if_none(release_branch.name)) if reintegrate is None: prompt_result = prompt( context=context, message=_("Branches may be reintegrated upon discontinuation."), prompt=_("Do you want to reintegrate {branch} into {base_branch}?" ).format(branch=repr(release_branch.short_name), base_branch=repr(base_branch_ref.short_name)), ) command_context.add_subresult(prompt_result) if command_context.has_errors(): return context.result reintegrate = prompt_result.value if not command_context.has_errors(): # run merge on local clone clone_result = clone_repository(context, context.config.release_branch_base) clone_context: Context = create_temp_context(context, result, clone_result.value.dir) clone_context.config.remote_name = 'origin' changes = list() if reintegrate: git_or_fail( clone_context.repo, command_context.result, ['checkout', base_branch_ref.short_name], _("Failed to checkout branch {branch_name}.").format( branch_name=repr(base_branch_ref.short_name))) git_or_fail( clone_context.repo, command_context.result, ['merge', '--no-ff', release_branch_info.upstream.name], _("Failed to merge work branch.\n" "Rebase {work_branch} on {base_branch} and try again" ).format(work_branch=repr(release_branch.short_name), base_branch=repr(base_branch_ref.short_name))) changes.append( _("{branch} reintegrated into {base_branch}").format( branch=repr(release_branch.name), base_branch=repr(base_branch_ref.name))) changes.append(_("Discontinuation tag")) prompt_result = prompt_for_confirmation( context=context, fail_title=_("Failed to discontinue {branch}.").format( branch=repr(release_branch.name)), message=(" - " + (os.linesep + " - ").join([_("Changes to be pushed:")] + changes)), prompt=_("Continue?"), ) command_context.add_subresult(prompt_result) if command_context.has_errors() or not prompt_result.value: return context.result 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_command.append( base_branch_ref.name + ':' + repotools.create_ref_name( const.LOCAL_BRANCH_PREFIX, base_branch_ref.short_name)) push_command.append('--force-with-lease=' + repotools.create_ref_name( const.LOCAL_TAG_PREFIX, discontinuation_tag_name) + ':') push_command.append( repotools.ref_target(release_branch) + ':' + repotools.create_ref_name(const.LOCAL_TAG_PREFIX, discontinuation_tag_name)) git_or_fail(clone_context.repo, command_context.result, push_command) fetch_all_and_ff(context.repo, command_context.result, context.config.remote_name) return context.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
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