Пример #1
0
class GitRepo:
    def __init__(self, config: Config, path: str = None) -> None:
        self.__config: Config = config
        try:
            if not path:
                path = config.root_path
            self.__repo = Repo(path)
            assert not self.__repo.bare
            self.origin = self.__repo.remote('origin')
        except InvalidGitRepositoryError:
            log_error(
                "Path " + path +
                " is not a git repository. Please go to valid git repository!")
            sys.exit()

    def pull(self, branch_name: str = None):
        if not branch_name:
            branch = self.__repo.active_branch.name
        else:
            branch = branch_name
        try:
            log_info('Pull changes from origin ...')
            self.__repo.git.execute(
                "git pull origin {}".format(branch).split(" "))
        except GitCommandError:
            log_error(
                "Pull from origin/" + branch + " on " +
                self.__repo.working_tree_dir +
                " is not possible as you might have uncommitted or untracked files. Fix the working tree, and then try again."
            )
            if not prompt_yesno_question(
                    "Did you fix the issue manually? Resume script?"):
                self.reset()
                sys.exit()

    def reset(self):
        if (self.__config.cleanup_silently or prompt_yesno_question(
                'Should the repository and file system to be reset automatically?\nThis will reset the entire repository inlcuding latest commits to comply to remote.\nThis will also delete untrackted files!'
        )):
            # arbitrary 20, but extensive enough to reset all hopefully
            log_info("Executing reset (git reset --hard HEAD~20)")
            self.__repo.git.reset('--hard', 'HEAD~20')
            self.update_and_clean()

    def update_and_clean(self):
        log_info(
            "Executing update and cleanup (git pull origin && git clean -fd)")
        self.pull()
        self.__repo.git.clean("-fd")
        if not self.is_working_copy_clean():
            log_error(
                "Reset and cleanup did not work out. Other branches have local commits not yet pushed:"
            )
            log_info("\n" + self.__list_unpushed_commits())
            if not prompt_yesno_question(
                    "Something went wrong during cleanup. Please check if you can perform the cleanup on your own. Resume the script?"
            ):
                self.reset()
                sys.exit()

    def checkout(self, branch_name):
        log_info("Checkout " + branch_name)
        self.__repo.git.checkout(branch_name)
        self.update_and_clean()

    def commit(self, commit_message: str):
        try:
            if self.__list_uncommitted_files() != "":
                log_info("Committing ...")
                self.__repo.index.commit("#" +
                                         str(self.__config.github_issue_no) +
                                         " " + commit_message)
            else:
                log_info("Nothing to commit.")
        except Exception as e:
            if "no changes added to commit" in str(e):
                log_info("No File is changed, Nothing to commit..")

    def push(self, force: bool = False):
        ''' Boolean return type states, whether to continue process or abort'''
        if (not force and not self.has_unpushed_commits()):
            log_info("Nothing to be pushed.")
            return

        if (self.__config.test_run
                or self.__config.debug) and not prompt_yesno_question(
                    "[DEBUG] Changes will be pushed now. Continue?"):
            self.reset()
            sys.exit()
        if self.__config.dry_run:
            log_info_dry('Skipping pushing of changes.')
            return

        try:
            log_info("Pushing to origin/" + self.__repo.active_branch.name +
                     " in " + self.__repo.working_tree_dir + "  ...")
            self.__repo.git.execute("git push origin " +
                                    self.__repo.active_branch.name + " --tags")
        except Exception as e:
            if "no changes added to commit" in str(e):
                log_info("No file is changed, nothing to commit.")
            else:
                if not prompt_yesno_question(
                        "Something went wrong during pushing. Please check if you can perform pushing on your own. Resume the script?"
                ):
                    self.reset()

    def add_modified_files(self) -> None:
        self.__repo.git.add(u=True)

    def merge(self, source: str, target: str) -> None:
        if self.__config.dry_run:
            log_info_dry("Would merge from " + source + " to " + target)
            return

        try:
            self.checkout(target)
            log_info("Executing git pull...")
            self.pull()
            log_info("Merging...")
            self.__repo.git.execute("git merge " +
                                    self.__config.branch_to_be_released)
            log_info(
                "Adapting automatically generated merge commit message to include issue no."
            )
            automatic_commit_message = self.__repo.git.execute(
                "git log -1 --pretty=%B")
            if "Merge" in automatic_commit_message and str(
                    self.__config.github_issue_no
            ) not in automatic_commit_message:
                self.__repo.git.execute('git commit --amend -m"#' +
                                        str(self.__config.github_issue_no) +
                                        ' ' + automatic_commit_message + '"')
        except Exception as ex:
            log_error(
                "Something went wrong, please check if merge conflicts exist and solve them."
            )
            if self.__config.debug:
                print(ex)
            if not prompt_yesno_question(
                    "If there were conflicts you solved and committed, would you like to resume the script?"
            ):
                self.__repo.git.execute("git merge --abort")
                self.reset()
                sys.exit()

    def update_documentation(self) -> None:
        log_info("Changing the " + self.__config.wiki_version_overview_page +
                 " file, updating the version number...")
        version_decl = self.__config.cobigenwiki_title_name
        new_version_decl = version_decl + " v" + self.__config.release_version
        modified_file = os.path.join(self.__config.root_path, "documentation",
                                     self.__config.wiki_version_overview_page)
        with FileInput(modified_file, inplace=True) as file:
            for line in file:
                line = re.sub(r'' + version_decl + r'\s+v[0-9]\.[0-9]\.[0-9]',
                              new_version_decl, line)
                sys.stdout.write(line)

        self.add_modified_files()

    def exists_tag(self, tag_name) -> bool:
        return tag_name in self.__repo.tags

    def get_changed_files_of_last_commit(self) -> List[str]:
        return str(
            self.__repo.git.execute("git diff HEAD^ HEAD --name-only".split(
                " "))).strip().splitlines()

    def create_tag_on_last_commit(self) -> None:
        self.__repo.create_tag(self.__config.tag_name)
        log_info("Git tag " + self.__config.tag_name + " created!")

    def assure_clean_working_copy(self) -> None:
        if not self.is_working_copy_clean(True):
            log_error("Working copy is not clean!")
            if self.__config.cleanup_silently or prompt_yesno_question(
                    "Should I clean the repo for you? This will delete all untracked files and hardly reset the repository!"
            ):
                self.reset()
            else:
                log_info(
                    "Please cleanup your working copy first. Then run the script again."
                )
                sys.exit()
        else:
            log_info("Working copy clean.")

    def is_working_copy_clean(self, check_all_branches=False) -> bool:
        return self.__repo.git.execute("git diff --shortstat".split(
            " ")) == "" and not self.has_uncommitted_files(
            ) and not self.has_unpushed_commits()

    def __list_uncommitted_files(self) -> str:
        return self.__repo.git.execute("git status --porcelain".split(" "))

    def has_uncommitted_files(self) -> bool:
        return self.__list_uncommitted_files() != ""

    def __list_unpushed_commits(self) -> str:
        # just check for the current branch
        return self.__repo.git.execute("git log origin/" +
                                       self.__get_current_branch_name() +
                                       "..HEAD")
        # check for all branches:
        # return self.__repo.git.execute("git log --branches --not --remotes".split(" "))

    def __get_current_branch_name(self) -> str:
        return self.__repo.git.execute("git rev-parse --abbrev-ref HEAD")

    def has_unpushed_commits(self) -> bool:
        return self.__list_unpushed_commits() != ""

    def __is_tracked_and_dirty(self, path: str) -> bool:
        changed = [item.a_path for item in self.__repo.index.diff(None)]
        changedAbs = [
            os.path.abspath(
                os.path.join(self.__repo.working_tree_dir, item.a_path))
            for item in self.__repo.index.diff(None)
        ]
        log_debug("Untracked and Dirty files: " + str(changed))
        if path in changed or path in changedAbs:
            # modified
            return True
        else:
            return False

    def close(self):
        log_info("Close repository")
        self.__repo.close()