From 291b9aa0cb0342a2db2a80ea5d3531dd9876be14 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Tue, 14 Jun 2022 10:29:19 -0500 Subject: [PATCH] Added git commands - gitpython added as a prod requirement - GitError raised when git command fails - added functions encompassing key functionality --- cookie_composer/composition.py | 30 ++++++------ cookie_composer/exceptions.py | 6 +++ cookie_composer/git_commands.py | 82 +++++++++++++++++++++++++++++++++ requirements/cookiecutter.txt | 2 +- requirements/dev.txt | 22 +++++---- requirements/docs.txt | 10 ++-- requirements/prod.in | 1 + requirements/prod.txt | 8 +++- requirements/test.txt | 12 ++++- 9 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 cookie_composer/git_commands.py diff --git a/cookie_composer/composition.py b/cookie_composer/composition.py index cf7f9e5..f842999 100644 --- a/cookie_composer/composition.py +++ b/cookie_composer/composition.py @@ -234,6 +234,21 @@ def write_composition(layers: List[LayerConfig], destination: Union[str, Path]): yaml.dump_all(dict_layers, f) +def write_rendered_composition(composition: RenderedComposition): + """ + Write the composition file using the rendered layers to the appropriate. + + Args: + composition: The rendered composition object to export + """ + layers = [] + for rendered_layer in composition.layers: + layers.append(rendered_layer.layer) + + composition_file = composition.render_dir / composition.rendered_name / ".composition.yaml" + write_composition(layers, composition_file) + + def get_merge_strategy(path: Path, merge_strategies: Dict[str, str]) -> str: """ Return the merge strategy of the path based on the layer configured rules. @@ -261,18 +276,3 @@ def get_merge_strategy(path: Path, merge_strategies: Dict[str, str]) -> str: break return strategy - - -def write_rendered_composition(composition: RenderedComposition): - """ - Write the composition file using the rendered layers to the appropriate. - - Args: - composition: The rendered composition object to export - """ - layers = [] - for rendered_layer in composition.layers: - layers.append(rendered_layer.layer) - - composition_file = composition.render_dir / composition.rendered_name / ".composition.yaml" - write_composition(layers, composition_file) diff --git a/cookie_composer/exceptions.py b/cookie_composer/exceptions.py index 9e975cb..803c9bd 100644 --- a/cookie_composer/exceptions.py +++ b/cookie_composer/exceptions.py @@ -24,3 +24,9 @@ def __init__( msg = f"There was a problem merging {origin} and {destination} using {strategy}: {error_message}" super().__init__(msg) super().__init__(error_message) + + +class GitError(Exception): + """There was a problem doing git operations.""" + + pass diff --git a/cookie_composer/git_commands.py b/cookie_composer/git_commands.py new file mode 100644 index 0000000..6766d12 --- /dev/null +++ b/cookie_composer/git_commands.py @@ -0,0 +1,82 @@ +"""Functions for using git.""" +from ctypes import Union +from pathlib import Path + +from git import InvalidGitRepositoryError, Repo + +from cookie_composer.exceptions import GitError + + +def get_repo(project_dir: Union[str, Path]) -> Repo: + """ + Get the git Repo object for a directory. + + Args: + project_dir: The directory containing the .git folder + + Raises: + GitError: If the directory is not a git repo + + Returns: + The GitPython Repo object + """ + try: + return Repo(str(project_dir)) + except InvalidGitRepositoryError as e: + raise GitError("Some cookie composer commands only work on git repositories.") from e + + +def branch_exists(repo: Repo, branch_name: str) -> bool: + """ + Does the branch exist in the repo? + + Args: + repo: The repository to check + branch_name: The name of the branch to check for + + Returns: + ``True`` if the branch exists + """ + return branch_name in repo.refs + + +def remote_branch_exists(repo: Repo, branch_name: str, remote_name: str = "origin") -> bool: + """ + Does the branch exist in the remote repo? + + Args: + repo: The repository to check + branch_name: The name of the branch to check for + remote_name: The name of the remote reference. Defaults to ``origin`` + + Returns: + ``True`` if the branch exists in the remote repository + """ + return branch_name in repo.remotes[remote_name].refs + + +def checkout_branch(repo: Repo, branch_name: str, remote_name: str = "origin"): + """Checkout a local or remote branch.""" + if repo.is_dirty(): + raise GitError( + "Cookie composer cannot apply updates on an unclean git project." + " Please make sure your git working tree is clean before proceeding." + ) + repo.remotes[0].fetch() + if branch_exists(repo, branch_name): + repo.heads[branch_name].checkout() + elif remote_branch_exists(repo, branch_name, remote_name): + repo.create_head(branch_name, f"origin/{branch_name}") + repo.heads[branch_name].checkout() + + +def branch_from_first_commit(repo: Repo, branch_name: str): + """Create and checkout a branch from the repo's first commit.""" + if repo.is_dirty(): + raise GitError( + "Cookie composer cannot apply updates on an unclean git project." + " Please make sure your git working tree is clean before proceeding." + ) + first_commit = list(repo.iter_commits("HEAD", max_parents=0, max_count=1))[0] + repo.create_head(branch_name, first_commit.hexsha) + repo.heads[branch_name].checkout() diff --git a/requirements/cookiecutter.txt b/requirements/cookiecutter.txt index 5dd4482..e09bf27 100644 --- a/requirements/cookiecutter.txt +++ b/requirements/cookiecutter.txt @@ -32,7 +32,7 @@ python-slugify==6.1.2 # via -r cookiecutter.in pyyaml==6.0 # via -r cookiecutter.in -requests==2.27.1 +requests==2.28.0 # via -r cookiecutter.in six==1.16.0 # via python-dateutil diff --git a/requirements/dev.txt b/requirements/dev.txt index 4cdeef2..e0fc916 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -73,7 +73,7 @@ distlib==0.3.4 # via # -r test.txt # virtualenv -docutils==0.17.1 +docutils==0.18.1 # via # -r docs.txt # myst-parser @@ -96,9 +96,13 @@ ghp-import==2.1.0 git-fame==1.15.1 # via -r dev.in gitdb==4.0.9 - # via gitpython + # via + # -r test.txt + # gitpython gitpython==3.1.27 - # via generate-changelog + # via + # -r test.txt + # generate-changelog identify==2.5.1 # via # -r test.txt @@ -162,7 +166,7 @@ mypy-extensions==0.4.3 # via # -r test.txt # black -myst-parser==0.17.2 +myst-parser==0.18.0 # via -r docs.txt nodeenv==1.6.0 # via @@ -249,7 +253,7 @@ pyyaml==6.0 # -r test.txt # myst-parser # pre-commit -requests==2.27.1 +requests==2.28.0 # via # -r docs.txt # -r test.txt @@ -277,7 +281,9 @@ six==1.16.0 # python-dateutil # virtualenv smmap==5.0.0 - # via gitdb + # via + # -r test.txt + # gitdb snowballstemmer==2.2.0 # via # -r docs.txt @@ -286,7 +292,7 @@ soupsieve==2.3.2.post1 # via # -r docs.txt # beautifulsoup4 -sphinx==4.5.0 +sphinx==5.0.1 # via # -r docs.txt # furo @@ -295,7 +301,7 @@ sphinx==4.5.0 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.18.2 +sphinx-autodoc-typehints==1.18.3 # via -r docs.txt sphinx-basic-ng==0.0.1a11 # via diff --git a/requirements/docs.txt b/requirements/docs.txt index 57ae158..4058130 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -22,7 +22,7 @@ click==8.1.3 # via # -c test.txt # sphinx-click -docutils==0.17.1 +docutils==0.18.1 # via # myst-parser # sphinx @@ -58,7 +58,7 @@ mdit-py-plugins==0.3.0 # via myst-parser mdurl==0.1.1 # via markdown-it-py -myst-parser==0.17.2 +myst-parser==0.18.0 # via -r docs.in packaging==21.3 # via @@ -83,7 +83,7 @@ pyyaml==6.0 # via # -c test.txt # myst-parser -requests==2.27.1 +requests==2.28.0 # via # -c test.txt # sphinx @@ -95,7 +95,7 @@ snowballstemmer==2.2.0 # via sphinx soupsieve==2.3.2.post1 # via beautifulsoup4 -sphinx==4.5.0 +sphinx==5.0.1 # via # -r docs.in # furo @@ -104,7 +104,7 @@ sphinx==4.5.0 # sphinx-basic-ng # sphinx-click # sphinx-copybutton -sphinx-autodoc-typehints==1.18.2 +sphinx-autodoc-typehints==1.18.3 # via -r docs.in sphinx-basic-ng==0.0.1a11 # via furo diff --git a/requirements/prod.in b/requirements/prod.in index cebcdac..f4f1c15 100644 --- a/requirements/prod.in +++ b/requirements/prod.in @@ -1,6 +1,7 @@ -r cookiecutter.txt fsspec +gitpython pydantic requests rich-click diff --git a/requirements/prod.txt b/requirements/prod.txt index 80a256e..6e4b11b 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -30,6 +30,10 @@ commonmark==0.9.1 # via rich fsspec==2022.5.0 # via -r prod.in +gitdb==4.0.9 + # via gitpython +gitpython==3.1.27 + # via -r prod.in idna==3.3 # via # -r cookiecutter.txt @@ -56,7 +60,7 @@ python-slugify==6.1.2 # via -r cookiecutter.txt pyyaml==6.0 # via -r cookiecutter.txt -requests==2.27.1 +requests==2.28.0 # via # -r cookiecutter.txt # -r prod.in @@ -72,6 +76,8 @@ six==1.16.0 # via # -r cookiecutter.txt # python-dateutil +smmap==5.0.0 + # via gitdb text-unidecode==1.3 # via # -r cookiecutter.txt diff --git a/requirements/test.txt b/requirements/test.txt index c542216..a03c972 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -49,6 +49,12 @@ flake8==4.0.1 # via -r test.in fsspec==2022.5.0 # via -r prod.txt +gitdb==4.0.9 + # via + # -r prod.txt + # gitpython +gitpython==3.1.27 + # via -r prod.txt identify==2.5.1 # via pre-commit idna==3.3 @@ -118,7 +124,7 @@ pyyaml==6.0 # via # -r prod.txt # pre-commit -requests==2.27.1 +requests==2.28.0 # via -r prod.txt rich==12.4.4 # via @@ -137,6 +143,10 @@ six==1.16.0 # -r prod.txt # python-dateutil # virtualenv +smmap==5.0.0 + # via + # -r prod.txt + # gitdb text-unidecode==1.3 # via # -r prod.txt