def for_branch(cls, branch_name: str) -> Optional['GaeApp']: """ Return app configuration for the given branch. This will look for the configuration in the ``appengine.projects`` config variable. Args: branch_name (str): The name of the branch we want the configuration for. Returns: Optional[GaeApp]: The `GaeApp` instance with the configuration for the project. None: If no project configuration can be found. """ for proj in conf.get('appengine.projects', []): if fnmatch(branch_name, proj['branch']): proj = dict(proj) proj.pop('branch') proj['deployables'] = list( frozenset( itertools.chain(conf.get('appengine.deployables', []), proj.get('deployables', [])))) return cls(**proj) return None
def merged(): """ Cleanup the release branch after it was remotely merged to master. """ develop = conf.get('git.devel_branch', 'develop') master = conf.get('git.master_branch', 'master') branch = git.current_branch(refresh=True) common.assert_branch_type('release') hooks.register.call('pre-release-merged', branch) # Pull master with the merged release common.git_checkout(master) common.git_pull(master) # Merge to develop common.git_checkout(develop) common.git_pull(develop) common.git_merge(develop, branch.name) # Cleanup common.git_branch_delete(branch.name) common.git_prune() common.git_checkout(develop) hooks.register.call('post-release-merged', branch)
def _manage_cmd(cmd, settings=None): # type: () -> None """ Run django ./manage.py command manually. This function eliminates the need for having ``manage.py`` (reduces file clutter). """ import sys from os import environ from peltak.core import conf from peltak.core import context from peltak.core import log sys.path.insert(0, conf.get('src_dir')) settings = settings or conf.get('django.settings', None) environ.setdefault("DJANGO_SETTINGS_MODULE", settings) args = sys.argv[0:-1] + cmd if context.get('pretend', False): log.info("Would run the following manage command:\n<90>{}", args) else: from django.core.management import execute_from_command_line execute_from_command_line(args)
def get_version_files() -> List[VersionFile]: version_files = conf.get('version.files', []) if not version_files: # TODO: 'version.file' is deprecated, use 'version.files' instead. single = conf.get('version.file', None) version_files = [single] if single else [] return [load_version_file(p) for p in version_files]
def clean(exclude: List[str]): """ Remove all unnecessary files. Args: exclude (list[str]): A list of path patterns to exclude from deletion. """ pretend = context.get('pretend', False) exclude = list(exclude) + conf.get('clean.exclude', []) clean_patterns = conf.get('clean.patterns', [ '*__pycache__*', '*.py[cod]', '*.swp', "*.mypy_cache", "*.pytest_cache", "*.build", ]) if context.get('verbose'): log.info('Clean patterns:') for pattern in clean_patterns: log.info(f' <90>{pattern}') log.info('Exclude:') for pattern in exclude: log.info(f' <90>{pattern}') num_files = 0 with util.timed_block() as t: files = fs.filtered_walk(conf.proj_path(), clean_patterns, exclude) log.info('') log.info('Deleting:') for path in files: try: num_files += 1 if not isdir(path): log.info(' <91>[file] <90>{}', path) if not pretend: os.remove(path) else: log.info(' <91>[dir] <90>{}', path) if not pretend: rmtree(path) except OSError: log.info("<33>Failed to remove <90>{}", path) if pretend: msg = "Would delete <33>{}<32> files. Took <33>{}<32>s" else: msg = "Deleted <33>{}<32> files in <33>{}<32>s" log.info(msg.format(num_files, t.elapsed_s))
def finish(fast_forward: bool): """ Merge current feature branch into develop. """ pretend = context.get('pretend', False) if not pretend and (git.staged() or git.unstaged()): log.err("You have uncommitted changes in your repo!\n" "You need to stash them before you merge the feature branch") sys.exit(1) develop = conf.get('git.devel_branch', 'develop') branch = git.current_branch(refresh=True) common.assert_branch_type('feature') hooks.register.call('pre-feature-finish', branch) # Merge feature into develop common.git_checkout(develop) common.git_pull(develop) common.git_merge(develop, branch.name, no_ff=not fast_forward) # Cleanup common.git_branch_delete(branch.name) common.git_prune() common.git_checkout(develop) hooks.register.call('post-feature-finish', branch)
def post_conf_load(): """ After the config was loaded, register all scripts as click commands. """ scripts = conf.get('scripts', {}) for name, script_conf in scripts.items(): script = Script.from_config(name, script_conf) script.register(root_cli if script.root_cli else run_cli)
def update(): """ Update the feature with updates committed to develop. This will merge current develop into the current branch. """ branch = git.current_branch(refresh=True) develop = conf.get('git.devel_branch', 'develop') common.assert_branch_type('feature') common.git_checkout(develop) common.git_pull(develop) common.git_checkout(branch.name) common.git_merge(branch.name, develop)
def finish(fast_forward: bool): """ Merge current release into develop and master and tag it. """ pretend = context.get('pretend', False) if not pretend and (git.staged() or git.unstaged()): log.err("You have uncommitted changes in your repo!\n" "You need to stash them before you merge the release branch") sys.exit(1) develop = conf.get('git.devel_branch', 'develop') master = conf.get('git.master_branch', 'master') branch = git.current_branch(refresh=True) common.assert_branch_type('release') hooks.register.call('pre-release-finish', branch) # Merge release into master common.git_checkout(develop) common.git_pull(develop) common.git_merge(develop, branch.name, no_ff=not fast_forward) # Merge release into develop common.git_checkout(master) common.git_pull(master) common.git_merge(master, branch.name, no_ff=not fast_forward) # Tag the release commit with version number tag(changelog()) # Cleanup common.git_branch_delete(branch.name) common.git_prune() common.git_checkout(master) hooks.register.call('post-release-finish', branch)
def choose_branch(exclude: Optional[Iterable[str]] = None) -> str: """ Show the user a menu to pick a branch from the existing ones. Args: exclude (list[str]): List of branch names to exclude from the menu. By default it will exclude master and develop branches. To show all branches pass an empty array here. Returns: str: The name of the branch chosen by the user. If the user inputs an invalid choice, he will be asked again (and again) until he picks a a valid branch. """ if exclude is None: master = conf.get('git.master_branch', 'master') develop = conf.get('git.devel_branch', 'develop') exclude = {master, develop} branches = list(set(git.branches()) - set(exclude)) # Print the menu for i, branch_name in enumerate(branches): shell.cprint('<90>[{}] <33>{}'.format(i + 1, branch_name)) # Get a valid choice from the user choice = 0 while choice < 1 or choice > len(branches): prompt = "Pick a base branch from the above [1-{}]".format( len(branches)) choice = click.prompt(prompt, value_proc=int) # type: ignore if not (1 <= choice <= len(branches)): fmt = "Invalid choice {}, you must pick a number between {} and {}" log.err(fmt.format(choice, 1, len(branches))) return branches[choice - 1]
def _get_all_changelog_items(start_rev: Optional[str], end_rev: Optional[str]) -> ChangelogItems: commits = _get_commits_in_range(start_rev, end_rev) tags = [ ChangelogTag(**x) for x in conf.get("changelog.tags", DEFAULT_TAGS) ] results: ChangelogItems = OrderedDict((tag.header, []) for tag in tags) for commit in commits: full_message = f"{commit.title}\n\n{commit.desc}" commit_items = extract_changelog_items(full_message, tags) for header, items in commit_items.items(): results[header] += items return results
def start(component: str, exact: str): """ Create a new release branch. Args: component (str): Version component to bump when creating the release. Can be *major*, *minor* or *patch*. exact (str): The exact version to set for the release. Overrides the component argument. This allows to re-release a version if something went wrong with the release upload. """ version_files = versioning.get_version_files() develop = conf.get('git.devel_branch', 'develop') common.assert_on_branch(develop) with conf.within_proj_dir(): out = shell.run('git status --porcelain', capture=True).stdout lines = out.split(os.linesep) has_changes = any(not line.startswith('??') for line in lines if line.strip()) if has_changes: log.info("Cannot release: there are uncommitted changes") exit(1) old_ver, new_ver = versioning.bump(component, exact) log.info("Bumping package version") log.info(" old version: <35>{}".format(old_ver)) log.info(" new version: <35>{}".format(new_ver)) with conf.within_proj_dir(): branch = 'release/' + new_ver hooks.register.call('pre-release-start', branch, old_ver, new_ver) common.git_checkout(branch, create=True) log.info("Creating commit for the release") shell.run('git add {files} && git commit -m "{msg}"'.format( files=' '.join(f'"{v.path}"' for v in version_files), msg="Releasing v{}".format(new_ver))) hooks.register.call('post-release-start', branch, old_ver, new_ver)
def start(name: str): """ Start working on a new feature by branching off develop. This will create a new branch off develop called feature/<name>. Args: name (str): The name of the new feature. """ feature_name = 'feature/' + common.to_branch_name(name) develop = conf.get('git.devel_branch', 'develop') common.assert_on_branch(develop) hooks.register.call('pre-feature-start', name) common.git_checkout(feature_name, create=True) hooks.register.call('post-feature-start', name)
def merged(): """ Cleanup a remotely merged branch. """ develop = conf.get('git.devel_branch', 'develop') branch = git.current_branch(refresh=True) common.assert_branch_type('feature') hooks.register.call('pre-feature-merged', branch) # Pull develop with the merged feature common.git_checkout(develop) common.git_pull(develop) # Cleanup common.git_branch_delete(branch.name) common.git_prune() common.git_checkout(develop) hooks.register.call('post-feature-merged', branch)
def extract_changelog_items(text: str, tags: List[ChangelogTag]) -> Dict[str, List[str]]: """ Extract all tagged items from text. Args: text (str): Text to extract the tagged items from. Each tagged item is a paragraph that starts with a tag. It can also be a text list item. Returns: tuple[list[str], list[str], list[str]]: A tuple of ``(features, changes, fixes)`` extracted from the given text. The tagged items are usually features/changes/fixes but it can be configured through `pelconf.yaml`. """ tag_format = conf.get("changelog.tag_format", DEFAULT_TAG_FORMAT) continuation_tag = conf.get("changelog.continuation_tag", DEFAULT_CONTINUATION_TAG) patterns = { tag.header: tag_re(tag_format.format(tag=tag.tag)) for tag in tags } more_pttrn = tag_re(tag_format.format(tag=continuation_tag)) items: ChangelogItems = {tag.header: [] for tag in tags} curr_tag = None curr_text = '' last_tag = None for line in text.splitlines(): if not line.strip(): if curr_tag is not None: items[curr_tag].append(curr_text) curr_text = '' last_tag = curr_tag curr_tag = None more_match = more_pttrn.match(line) if more_match and last_tag: # If it's a continuation tag, then just add it's text to the last # used tag. This only works if there was a previous tag. curr_tag = last_tag curr_text = items[last_tag][-1] items[last_tag] = items[last_tag][:-1] line = more_match.group('text') else: for tag in tags: m = patterns[tag.header].match(line) if m: if curr_tag is not None: # If we're already in a tag definition and we encountered # a beginning of a new tag, just finish by adding new # item to the current tag item list. items[curr_tag].append(curr_text) curr_text = '' curr_tag = tag.header line = m.group('text') break if curr_tag is not None: if more_match: curr_text = '{}\n{}'.format(curr_text, line.strip()).strip() else: curr_text = '{} {}'.format(curr_text.strip(), line.strip()).strip() if curr_tag is not None: items[curr_tag].append(curr_text) return items