cr-connect-workflow/crc/services/git_service.py

142 lines
5.8 KiB
Python

import os
from crc import app
from crc.api.common import ApiError
from crc.models.git_models import GitRepo
from git import Repo, InvalidGitRepositoryError, NoSuchPathError, GitCommandError
from datetime import datetime
class GitService(object):
"""This is a wrapper around GitPython to manage versioning and syncing
for Workflow Spec files that reside on the filesystem.
This is not a full-service git tool. It has many limitations.
This service requires environment variables:
SYNC_FILE_ROOT - An absolute path to the local Workflow Spec files. This is our repository.
GIT_REMOTE_PATH - Location of spec files on GitHub. Currently, this is "sartography/crconnect-workflow-specs"
GIT_BRANCH - The name of your local development branch. We force load this branch
GIT_MERGE_BRANCH - The branch that can be merged into GIT_BRANCH. I.e., for Production machine, this would be set to 'staging', or something similar.
GIT_USER_NAME - The GitHub account to use
GIT_USER_PASS - The GitHub token to use for account GIT_USER_NAME
"""
# TODO: Implement the GIT_MERGE_BRANCH feature
@staticmethod
def get_remote_url(remote_path):
host = app.config['GIT_REMOTE_SERVER']
username = app.config["GIT_USER_NAME"]
password = app.config["GIT_USER_PASS"]
remote_url = f"https://{username}:{password}@{host}/{remote_path}.git"
return remote_url
@staticmethod
def setup_repo(remote_path, directory):
remote_url = GitService.get_remote_url(remote_path)
repo = Repo.clone_from(remote_url, directory)
return repo
def __get_repo(self):
remote_path = app.config['GIT_REMOTE_PATH']
git_branch = app.config['GIT_BRANCH']
directory = app.config['SYNC_FILE_ROOT']
display_push = app.config['GIT_DISPLAY_PUSH']
display_merge = app.config['GIT_DISPLAY_MERGE']
try:
repo = Repo(directory)
except InvalidGitRepositoryError:
# Thrown if the given repository appears to have an invalid format.
# I believe this means there is no .git directory
if os.listdir(directory):
# If the directory is not empty, we let them decide how to fix it
raise ApiError(code='invalid_git_repo',
message=f'The directory {directory} is not empty, and is not a valid git repository. Please fix this before continuing.')
else:
# The directory is empty, so we setup the repo
repo = self.setup_repo(remote_path, directory)
except NoSuchPathError:
# The directory does not exist, so setup
repo = self.setup_repo(remote_path, directory)
except Exception as e:
app.logger.error(e)
raise ApiError(code='unknown_exception',
message=f'There was an unknown exception. Original message is: {e}')
try:
repo.git.checkout(git_branch)
except GitCommandError:
# The branch might not exist yet, so we create it and its remote ref
repo.git.branch(git_branch)
repo.git.checkout(git_branch)
repo.remotes.origin.push(refspec='{}:{}'.format(git_branch, f'{git_branch}'))
repo.remotes.origin.fetch()
remote_ref = repo.remotes.origin.refs[f'{git_branch}']
repo.active_branch.set_tracking_branch(remote_ref)
repo.display_push = display_push
repo.display_merge = display_merge
return repo
def _get_repo(self):
# This returns a gitpython Repo object
return self.__get_repo()
def get_repo(self):
# This returns an instance of crc.models.git_models.GitRepo,
# built from a gitpython Repo object
repo = self._get_repo()
repo_model = GitRepo().from_repo(repo)
return repo_model
def push_to_remote(self, comment=None):
if comment is None or comment.strip() == '':
comment = f"Git commit: {datetime.now()}"
repo = self._get_repo()
# get list of modified files
modified = [item.a_path for item in repo.index.diff(None) if item.change_type == 'M']
# get list of deleted files
deleted = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
# get list of untracked files
untracked_files = repo.untracked_files
if len(modified) > 0:
repo.index.add(modified)
repo.index.add(untracked_files)
if len(deleted) > 0:
repo.index.remove(deleted)
repo.index.commit(comment)
repo.remotes.origin.push()
return repo
def pull_from_remote(self):
repo = self._get_repo()
if not repo.is_dirty():
try:
repo.remotes.origin.pull()
except GitCommandError as ce:
raise ApiError(code='git_command_error',
message='Error Running Git Command:' + str(ce))
else:
raise ApiError(code='dirty_repo',
message='You have modified or untracked files. Please fix this before attempting to pull.')
return repo
def merge_with_branch(self, branch):
# https://stackoverflow.com/questions/36799362/how-do-you-merge-the-master-branch-into-a-feature-branch-with-gitpython#36802900
repo = self._get_repo()
repo.remotes.origin.fetch()
merge_branch = repo.remotes.origin.refs[branch]
base = repo.merge_base(repo.active_branch, merge_branch)
repo.index.merge_tree(merge_branch, base=base)
repo.index.commit(f'Merge {branch} into working branch', parent_commits=(repo.active_branch.commit, merge_branch.commit))
repo.active_branch.checkout(force=True)
return repo