diff --git a/git_operator/changelog.py b/git_operator/changelog.py index 7b35096..6c55fe9 100644 --- a/git_operator/changelog.py +++ b/git_operator/changelog.py @@ -71,19 +71,19 @@ def bump_version(current: str, major: bool = False, minor: bool = False, patch: def get_changelog_markdown(ver: str, changelog: Dict) -> str: changelog_lines = [f'# Release version {ver}', ] - if bool(changelog['Major']): + if bool(changelog.get('Major', [])): changelog_lines.append('## Major changes') for line in changelog['Major']: changelog_lines.append(f'- {line["short_id"]} {line["title"]}') - if bool(changelog['Minor']): + if bool(changelog.get('Minor', [])): changelog_lines.append('## Minor changes') for line in changelog['Minor']: changelog_lines.append(f'- {line["short_id"]} {line["title"]}') - if bool(changelog['Patch']): + if bool(changelog.get('Patch', [])): changelog_lines.append('## Patches') for line in changelog['Patch']: changelog_lines.append(f'- {line["short_id"]} {line["title"]}') - if bool(changelog['Missing']): + if bool(changelog.get('Missing', [])): changelog_lines.append('## Missing definition') for line in changelog['Missing']: changelog_lines.append(f'- {line["short_id"]} {line["title"]}') diff --git a/git_operator/main.py b/git_operator/main.py index 49946e8..8c7f816 100644 --- a/git_operator/main.py +++ b/git_operator/main.py @@ -1,63 +1,41 @@ import os import argparse from repository import GitLabRepo -from changelog import get_latest_version - - -def release(service: GitLabRepo, args_ref: str = None): - tags = service.get_tags_as_string() - latest_vname = get_latest_version(list(tags.keys())) - if not bool(tags): - success = service.create_first_release(latest_vname) - exit(1 - int(success)) - latest_v = tags[latest_vname] - - latest_commit = service.get_commit(latest_v.target) - if not bool(args_ref): - args_ref = 'master' - ref = service.get_commit(args_ref) - - if ref is None: - print('Cannot find ref') - exit(0) - - diff = service.get_diff(to_ref=ref.id, from_ref=latest_commit.id) - - if not bool(diff['commits']): - print('There is no change') - exit(0) - success = service.create_release(ref, latest_vname, diff) - exit(1 - int(success)) - - -def hotfix(service: GitLabRepo, version: str, ref: str = None): - return service.create_hotfix(version, ref) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Git operator service') parser.add_argument('service', help='Service name', choices=['gitlab', 'github']) parser.add_argument('project_id', help='ID of Project', type=int) - parser.add_argument('command', help='Command', choices=['release', 'hotfix']) + parser.add_argument('command', help='Command', choices=['create-branch', 'tag', 'release']) parser.add_argument('--host', help='Git host', required=False, dest='host', default=os.getenv('GIT_HOST', '')) parser.add_argument('--token', help='Token for authentication', required=False, dest='token', default=os.getenv('GIT_PRIVATE_TOKEN', '')) parser.add_argument('--ref', help='Ref name or commit hash', required=False, dest='ref') - parser.add_argument('--version', help='Version needs hotfixing', required=False, dest='version') + parser.add_argument('--version', help='Desired version', required=False, dest='version') args = parser.parse_args() if not bool(args.host): host = 'https://gitlab.com' service = GitLabRepo(args.host, args.token) service.set_project(args.project_id) - success = 0 # Unknown error - if args.command == 'release': - success = release(service, args.ref) - - if args.command == 'hotfix': - if not bool(args.version): - parser.error('version is required if command=hotfix') - success = hotfix(service, args.version, args.ref) - exit(1 - int(success)) + if not(args.ref): + ref_name = 'master' + else: + ref_name = args.ref + + if args.command == 'create-branch' or args.command == 'release': + version, changes_log = service.get_next_version(ref_name, args.version) + if version is None: + exit(1) + success = service.create_version_branch(version, ref_name) + if not success: + exit(1) + + if args.command == 'tag' or args.command == 'release': + success = service.create_tag(ref_name, args.version) + if not success: + exit(1) + exit(0) \ No newline at end of file diff --git a/git_operator/repository.py b/git_operator/repository.py index 716623b..a69a30f 100644 --- a/git_operator/repository.py +++ b/git_operator/repository.py @@ -1,21 +1,46 @@ +"""Module for manipulating repository services +""" from typing import Dict, Optional from gitlab import Gitlab, GitlabGetError from gitlab.exceptions import GitlabCreateError from gitlab.v4.objects import Project, ProjectTag, ProjectCommit -from changelog import collect_changelog, bump_version, get_changelog_markdown, get_hotfix_changelog_markdown +from changelog import collect_changelog, bump_version, get_changelog_markdown, get_hotfix_changelog_markdown, get_latest_version class GitLabRepo: + """Class for Gitlab + """ __connector: Gitlab = None __project: Project = None def __init__(self, host, token) -> Gitlab: + """Init handler + + :param host: Repository host + :type host: str + :param token: Access Token + :type token: str + """ self.__connector = Gitlab(host, private_token=token) def set_project(self, project_id: int) -> Project: + """Set project as current project + + :param project_id: ID of project on Gitlab + :type project_id: int + :return: current project after set + :rtype: Project + """ self.__project = self.__connector.projects.get(project_id) def get_tags_as_string(self, project_id: Optional[int] = None) -> Dict[str, ProjectTag]: + """Get list tags as strings + + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: Dictionary with keys are tags as strings, values are ProjectTag objects + :rtype: Dict[str, ProjectTag] + """ result = {} if bool(project_id): project = self.__connector.projects.get(project_id) @@ -27,7 +52,16 @@ def get_tags_as_string(self, project_id: Optional[int] = None) -> Dict[str, Proj result[sem_ver] = record return result - def get_commit(self, commit_ref: str = 'master', project_id: Optional[int] = None) -> ProjectCommit: + def get_commit(self, commit_ref: str = 'master', project_id: Optional[int] = None) -> Optional[ProjectCommit]: + """Get commit information from commit ref + + :param commit_ref: Ref of commit to fetch, defaults to 'master' + :type commit_ref: str, optional + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: Commit object + :rtype: ProjectCommit or None + """ if bool(project_id): project = self.__connector.projects.get(project_id) else: @@ -37,7 +71,14 @@ def get_commit(self, commit_ref: str = 'master', project_id: Optional[int] = Non except GitlabGetError as ex: return None - def get_latest_commit(self, project_id: Optional[int] = None) -> ProjectCommit: + def get_latest_commit(self, project_id: Optional[int] = None) -> Optional[ProjectCommit]: + """Get latest commit + + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: [description] + :rtype: Optional[ProjectCommit] + """ if bool(project_id): project = self.__connector.projects.get(project_id) else: @@ -48,13 +89,39 @@ def get_latest_commit(self, project_id: Optional[int] = None) -> ProjectCommit: return None def get_diff(self, from_ref: str, to_ref: str = 'master', project_id: Optional[int] = None): + """Get differences commit between to refs + + :param from_ref: newer ref + :type from_ref: str + :param to_ref: older ref, defaults to 'master' + :type to_ref: str, optional + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: [description] + :rtype: [type] + """ if bool(project_id): project = self.__connector.projects.get(project_id) else: project = self.__project return project.repository_compare(from_ref, to_ref) - def create_first_release(self, version: str = '1.0.0', project_id: Optional[int] = None) -> bool: + def create_version_branch(self, version: str, ref: str, project_id: Optional[int] = None) -> bool: + """Create a branch with format `release/{version}` + + :param version: Desired version + :type version: str + :param ref: Ref to create branch from + :type ref: str + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: [description] + :rtype: bool + """ + if not bool(version) or not bool(ref): + print('Invalid version or ref') + return False + if bool(project_id): project = self.__connector.projects.get(project_id) else: @@ -63,117 +130,93 @@ def create_first_release(self, version: str = '1.0.0', project_id: Optional[int] try: project.branches.create({ 'branch': f'release/{version}', - 'ref': 'master', + 'ref': ref, }) except Exception as ex: print(f'{str(ex)}: release/{version}') return False - - try: - project.tags.create({ - 'tag_name': f'v{version}', - 'ref': 'master', - }) - except Exception as ex: - print(f'{str(ex)}: v{version}') - return False - - try: - project.releases.create({ - 'name': f'Release {version}', - 'tag_name': f'v{version}', - 'description': 'Init release', - }) - except Exception as ex: - print(f'{str(ex)}: {version}') - return False + print(f'Create branch release/{version}') return True - def create_release(self, from_ref: ProjectCommit, current_ver: str, diff: Dict, project_id: Optional[int] = None) -> bool: + def create_tag(self, ref_name: str, desired_version: Optional[str] = None, project_id: Optional[int] = None) -> bool: + """Create tag from a ref with or without desired version + + :param ref_name: Name of ref to create tag from + :type ref_name: str + :param desired_version: Desired version, not need to pass if make it auto, defaults to None + :type desired_version: Optional[str], optional + :param project_id: ID of project. If None then get from object, defaults to None + :type project_id: Optional[int], optional + :return: [description] + :rtype: bool + """ if bool(project_id): project = self.__connector.projects.get(project_id) else: project = self.__project - changelog = collect_changelog(diff) - new_version = bump_version(current_ver, bool(changelog['Major']), bool( - changelog['Minor']) or bool(changelog['Missing']), bool(changelog['Patch'])) - release_description = get_changelog_markdown(new_version, changelog) + tags = self.get_tags_as_string() - try: - project.branches.create({ - 'branch': f'release/{new_version}', - 'ref': from_ref.id, - }) - except GitlabCreateError as ex: - print(f'{str(ex)}: release/{new_version}') - if ex.error_message != 'Branch already exists': + # If exist then update tag, else create new tag + if desired_version in tags: + version = desired_version + latest_commit = self.get_commit(f'release/{version}') + diff = self.get_diff(to_ref=latest_commit.id, from_ref=tags[version].target) + if not bool(diff['commits']): + print('There is no differences') return False - except Exception as ex: - print(f'{str(ex)}: release/{new_version}') - return False + release_description = tags[version].release['description'] + changes_log = get_hotfix_changelog_markdown(release_description, diff) + # Delete tag & release + try: + project.releases.delete(id=f'v{version}') + project.tags.delete(id=f'v{version}') + except Exception as ex: + print(f'{str(ex)}: v{version}') + else: + version, changes_log = self.get_next_version(ref_name, desired_version) try: project.tags.create({ - 'tag_name': f'v{new_version}', - 'ref': from_ref.id, - 'release_description': release_description + 'tag_name': f'v{version}', + 'ref': ref_name, + 'release_description': changes_log }) - except GitlabCreateError as ex: - print(f'{str(ex)}: v{new_version}') - if ex.error_message != 'Tag already exists': - return False except Exception as ex: - print(f'{str(ex)}: v{new_version}') + print(f'{str(ex)}: v{version}') return False - + print(f'Create version {version}') return True - def create_hotfix(self, version: str, ref: Optional[str] = None, project_id: Optional[int] = None) -> bool: - if bool(project_id): - project = self.__connector.projects.get(project_id) - else: - project = self.__project + def get_next_version(self, ref_name: str = 'master', desired_version: Optional[str] = None): + """Get next version if capable + :param ref_name: ref name or ref hash to create next version, defaults to 'master' + :type ref_name: str, optional + :param desired_version: desired version to check if it is existed or not, defaults to None + :type desired_version: Optional[str], optional + :return: next version, change log + :rtype: Tuple(str, str) + """ tags = self.get_tags_as_string() - if version not in tags: - print('Cannot find ref') - return False - - # Collecting information - tag = tags[version] - tagging_commit = self.get_commit(tag.target) - release_description = tag.release['description'] - - if not bool(ref): - branch_name = f'release/{version}' - latest_commit = self.get_commit(branch_name) - else: - latest_commit = self.get_commit(ref) - diff = self.get_diff(to_ref=latest_commit.id, from_ref=tagging_commit.id) + latest_vname = get_latest_version(list(tags.keys())) + if not bool(tags): + if bool(desired_version): + latest_vname = desired_version + return latest_vname, get_changelog_markdown(latest_vname, {}) + ref = self.get_commit(ref_name) + if ref is None: + return None, '' + latest_tag = self.get_commit(tags[latest_vname].target) + diff = self.get_diff(from_ref=latest_tag.id, to_ref=ref.id) if not bool(diff['commits']): - print('There is not diffs') - return False - - # Append new change log - release_description = get_hotfix_changelog_markdown(release_description, diff) - - # Delete tag & release - try: - project.releases.delete(id=f'v{version}') - project.tags.delete(id=f'v{version}') - except Exception as ex: - print(f'{str(ex)}: v{version}') - - # Create new tag - try: - project.tags.create({ - 'tag_name': f'v{version}', - 'ref': latest_commit.id, - 'release_description': release_description - }) - except Exception as ex: - print(f'{str(ex)}: v{version}') - return False - - return True + return None, '' + changelog = collect_changelog(diff) + if bool(desired_version): + return desired_version, get_changelog_markdown(desired_version, changelog) + new_version = bump_version( + latest_vname, bool(changelog['Major']), + bool(changelog['Minor']) or bool(changelog['Missing']), + bool(changelog['Patch']) + ) + return new_version, get_changelog_markdown(new_version, changelog)