Repository: packit-service/packit
Pull request #49 (2019-02-08 11:02:02)

Local repo object representation
Add representation for LocalProject:
- cloned repo, working directory, branch
- git_url, repository, namespace
- GitProject, GitService from OGR

#### TODO:

- [x] Correct other CLI options to use this.
- [x] Better tests for ClonedRepo.
     - [x] more unit tests
     - [x] replace mock with flexmock
     - ~~[ ] integration tests~~ I'll leave this for know, since jscotka will create the structure of the integration tests.


Patch:
diff --git a/packit/cli/packit_base.py b/packit/cli/packit_base.py
index e8f51172..2bbf4800 100644
--- a/packit/cli/packit_base.py
+++ b/packit/cli/packit_base.py
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
 @click.option("-k", "--keytab")
 @click.option("-v", "--verbose", is_flag=True)
 @click.pass_context
-def sourcegit_base(ctx, **kwargs):
+def packit_base(ctx, **kwargs):
     ctx.obj = Config(**kwargs)
     if ctx.obj.debug:
         set_logging(level=logging.DEBUG)
@@ -37,11 +37,11 @@ def version():
     click.echo(packit.__version__)
 
 
-sourcegit_base.add_command(sg2dg)
-sourcegit_base.add_command(sg2srpm)
-sourcegit_base.add_command(watcher)
-sourcegit_base.add_command(version)
-sourcegit_base.add_command(watch_pr)
+packit_base.add_command(sg2dg)
+packit_base.add_command(sg2srpm)
+packit_base.add_command(watcher)
+packit_base.add_command(version)
+packit_base.add_command(watch_pr)
 
 if __name__ == '__main__':
-    sourcegit_base()
+    packit_base()
diff --git a/packit/cli/sourcegit_to_dist_git.py b/packit/cli/sourcegit_to_dist_git.py
index a6cc187b..1747562d 100644
--- a/packit/cli/sourcegit_to_dist_git.py
+++ b/packit/cli/sourcegit_to_dist_git.py
@@ -3,20 +3,17 @@ import logging
 import click
 
 from packit.config import pass_config, get_context_settings, get_local_package_config
+from packit.local_project import LocalProject
 from packit.transformator import Transformator
 
-logger = logging.getLogger("source_git")
+logger = logging.getLogger(__file__)
 
 
-@click.command("distgit", context_settings=get_context_settings())
+@click.command("sg2dg", context_settings=get_context_settings())
 @click.option("--dest-dir")
 @click.option("--no-new-sources", is_flag=True)
-@click.option("--package-name")
-@click.option("--rev-list-option", multiple=True)
 @click.option("--upstream-ref")
 @click.argument("repo")
-@click.argument("dist-git")
-@click.argument("name")
 @click.argument("version")
 @pass_config
 def sg2dg(config, dest_dir, no_new_sources, upstream_ref, repo, version):
@@ -35,20 +32,31 @@ def sg2dg(config, dest_dir, no_new_sources, upstream_ref, repo, version):
     """
 
     package_config = get_local_package_config()
+    sourcegit = LocalProject(git_url=repo)
+    if not package_config:
+        package_config = get_local_package_config(directory=sourcegit.working_dir)
+    distgit = LocalProject(
+        git_url=package_config.metadata["dist_git_url"],
+        namespace="rpms",
+        repo_name=package_config.metadata["package_name"],
+        working_dir=dest_dir,
+    )
+
     with Transformator(
-            url=repo,
-            version=version,
-            dest_dir=dest_dir,
-            fas_username=config.fas_user,
-            package_config=package_config,
+        sourcegit=sourcegit,
+        distgit=distgit,
+        version=version,
+        fas_username=config.fas_user,
+        package_config=package_config,
     ) as t:
-        t.clone_dist_git_repo()
         t.create_archive()
-        t.copy_synced_content_to_dest_dir(synced_files=package_config.synced_files)
+        t.copy_synced_content_to_distgit_directory(
+            synced_files=package_config.synced_files
+        )
         patches = t.create_patches(upstream=upstream_ref)
         t.add_patches_to_specfile(patch_list=patches)
         if not no_new_sources:
             t.upload_archive_to_lookaside_cache(config.keytab)
         else:
             logger.debug("Skipping fedpkg new-sources.")
-        click.echo(f"{t.dest_dir}")
+        click.echo(f"{distgit.working_dir}")
diff --git a/packit/cli/sourcegit_to_srpm.py b/packit/cli/sourcegit_to_srpm.py
index 9a1723d5..e7fe60f9 100644
--- a/packit/cli/sourcegit_to_srpm.py
+++ b/packit/cli/sourcegit_to_srpm.py
@@ -3,20 +3,16 @@ import logging
 import click
 
 from packit.config import pass_config, get_context_settings, get_local_package_config
+from packit.local_project import LocalProject
 from packit.transformator import Transformator
 
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__file__)
 
 
 @click.command("srpm", context_settings=get_context_settings())
 @click.option("--dest-dir")
-# @click.option("--no-new-sources", is_flag=True)
-@click.option("--package-name")
-@click.option("--rev-list-option", multiple=True)
 @click.option("--upstream-ref")
 @click.argument("repo")
-@click.argument("dist-git")
-@click.argument("name")
 @click.argument("version")
 @pass_config
 def sg2srpm(config, dest_dir, upstream_ref, repo, version):
@@ -35,18 +31,30 @@ def sg2srpm(config, dest_dir, upstream_ref, repo, version):
     """
 
     package_config = get_local_package_config()
+    sourcegit = LocalProject(git_url=repo)
+    if not package_config:
+        package_config = get_local_package_config(directory=sourcegit.working_dir)
+
+    distgit = LocalProject(
+        git_url=package_config.metadata["dist_git_url"],
+        namespace="rpms",
+        repo_name=package_config.metadata["package_name"],
+        working_dir=dest_dir,
+    )
+
+    distgit.working_dir_temporary = False
     with Transformator(
-        url=repo,
+        sourcegit=sourcegit,
+        distgit=distgit,
         version=version,
-        dest_dir=dest_dir,
-        branch=upstream_ref,
         fas_username=config.fas_user,
         package_config=package_config,
     ) as t:
-        t.clone_dist_git_repo()
         t.create_archive()
-        t.copy_synced_content_to_dest_dir(synced_files=package_config.synced_files)
+        t.copy_synced_content_to_distgit_directory(
+            synced_files=package_config.synced_files
+        )
         patches = t.create_patches(upstream=upstream_ref)
         t.add_patches_to_specfile(patch_list=patches)
-        t.create_srpm()
-        click.echo(f"{t.archive}")
+        specfile = t.create_srpm()
+        click.echo(specfile)
diff --git a/packit/config.py b/packit/config.py
index 42a4defb..0258572c 100644
--- a/packit/config.py
+++ b/packit/config.py
@@ -106,23 +106,24 @@ class PackageConfig:
         return Draft4Validator(PACKAGE_CONFIG_SCHEMA).is_valid(raw_dict)
 
 
-def get_local_package_config() -> Optional[PackageConfig]:
+def get_local_package_config(directory=None) -> Optional[PackageConfig]:
     """
     :return: local PackageConfig if present
     """
+    directory = directory or os.path.curdir
     for config_file_name in CONFIG_FILE_NAMES:
-
-        if os.path.isfile(config_file_name):
-            logger.debug(f"Local package config found: {config_file_name}")
+        config_file_name_full =os.path.join(directory, config_file_name)
+        if os.path.isfile(config_file_name_full):
+            logger.debug(f"Local package config found: {config_file_name_full}")
             try:
-                loaded_config = anymarkup.parse_file(config_file_name)
+                loaded_config = anymarkup.parse_file(config_file_name_full)
             except Exception as ex:
-                logger.error(f"Cannot load package config '{config_file_name}'.")
+                logger.error(f"Cannot load package config '{config_file_name_full}'.")
                 raise Exception(f"Cannot load package config: {ex}.")
 
             return parse_loaded_config(loaded_config=loaded_config)
 
-        logger.debug(f"The local config file '{config_file_name}' not found.")
+        logger.debug(f"The local config file '{config_file_name_full}' not found.")
 
     return None
 
diff --git a/packit/local_project.py b/packit/local_project.py
new file mode 100644
index 00000000..d54039df
--- /dev/null
+++ b/packit/local_project.py
@@ -0,0 +1,127 @@
+import logging
+import shutil
+
+import git
+
+from ogr.abstract import GitProject, GitService
+from packit.utils import is_git_repo, get_repo
+
+logger = logging.getLogger(__name__)
+
+
+class LocalProject:
+    def __init__(
+        self,
+        git_repo: git.Repo = None,
+        working_dir: str = None,
+        branch: str = None,
+        git_project: GitProject = None,
+        git_service: GitService = None,
+        git_url: str = None,
+        full_name: str = None,
+        namespace: str = None,
+        repo_name: str = None,
+    ) -> None:
+
+        self.git_repo = git_repo
+        self.working_dir = working_dir
+        self._branch = branch
+        self.git_project = git_project
+        self.git_service = git_service
+        self.git_url = git_url
+        self.full_name = full_name
+        self.repo_name = repo_name
+        self.namespace = namespace
+        self.working_dir_temporary = False
+
+        change = True
+        while change:
+            change = False
+
+            if self.repo_name and self.namespace and not self.full_name:
+                self.full_name = f"{self.namespace}/{self.repo_name}"
+                change = True
+
+            if self.full_name and not self.namespace:
+                self.namespace = self.full_name.split("/")[0]
+                change = True
+
+            if self.full_name and not self.repo_name:
+                self.repo_name = self.full_name.split("/")[1]
+                change = True
+
+            if (
+                self.repo_name
+                and self.namespace
+                and self.git_service
+                and not self.git_project
+            ):
+                self.git_project = self.git_service.get_project(
+                    repo=self.repo_name, namespace=self.namespace
+                )
+                change = True
+
+            if self.git_project and not self.git_service:
+                self.git_service = self.git_project.service
+                change = True
+
+            if self.git_repo and not self._branch:
+                self._branch = self.git_repo.active_branch
+                change = True
+
+            if self.git_repo and not self.working_dir:
+                self.working_dir = self.git_repo.working_dir
+                change = True
+
+            if self.working_dir and not self.git_repo:
+                if is_git_repo(directory=self.working_dir):
+                    self.git_repo = git.Repo(path=self.working_dir)
+                    change = True
+                elif self.git_url:
+                    self.git_repo = get_repo(
+                        url=self.git_url, directory=self.working_dir
+                    )
+                    change = True
+
+            if self.git_url and not self.working_dir and not self.git_repo:
+                self.git_repo = get_repo(url=self.git_url)
+                self.working_dir_temporary = True
+                change = True
+
+            if self.git_project and not self.git_url:
+                self.git_url = self.git_project.get_git_urls()["git"]
+                change = True
+
+            if self.git_project and not self.repo_name:
+                self.repo_name = self.git_project.repo
+                change = True
+
+            if self.git_project and not self.namespace:
+                self.namespace = self.git_project.namespace
+                change = True
+
+            if self.git_repo and not self.git_url:
+                self.git_url = self.git_repo.remote().urls[0]
+                change = True
+
+        if branch:
+
+            if branch not in self.git_repo.branches:
+                self.git_repo.create_head(self._branch)
+
+            self.git_repo.branches[self._branch].checkout()
+
+    def clean(self):
+        if self.working_dir_temporary:
+            logger.debug(f"Cleaning: {self.working_dir}")
+            shutil.rmtree(self.working_dir)
+            self.working_dir_temporary = False
+
+    @property
+    def branch(self):
+        if self.git_repo:
+            return self.git_repo.active_branch
+        return None
+
+    def __del__(self):
+        self.clean()
diff --git a/packit/sync.py b/packit/sync.py
index 97d4cf66..66b7ff3f 100644
--- a/packit/sync.py
+++ b/packit/sync.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import json
 import logging
-import shutil
 
 from ogr.abstract import GitService
 from ogr.services.github import GithubService
@@ -12,8 +11,9 @@ from packit.config import (
     get_packit_config_from_repo,
     get_local_package_config,
 )
+from packit.local_project import LocalProject
 from packit.transformator import Transformator
-from packit.utils import commits_to_nice_str, checkout_pr, get_repo
+from packit.utils import commits_to_nice_str, checkout_pr
 
 logger = logging.getLogger(__name__)
 
@@ -99,6 +99,11 @@ class Synchronizer:
                 title=fedmsg_dict["msg"]["pull_request"]["title"],
                 package_config=package_config,
             )
+        except ConnectionError as ex:
+            # TODO: Retry on connection error
+            logger.warning(f"Connection error on processing a msg {msg_id}")
+            logger.debug(str(ex))
+            return
         except Exception as ex:
             logger.warning(f"Error on processing a msg {msg_id}")
             logger.debug(str(ex))
@@ -131,69 +136,64 @@ class Synchronizer:
         :return:
         """
         logger.info("starting sync for project %s", target_url)
-        repo = get_repo(url=target_url, directory=repo_directory)
-        try:
-            checkout_pr(repo=repo, pr_id=pr_id)
-
-            with Transformator(
-                    url=target_url,
-                    repo=repo,
-                    branch=repo.active_branch,
-                    package_config=package_config,
-            ) as transformator:
-                dist_git_branch_name = f"source-git-{pr_id}"
-                transformator.clone_dist_git_repo(
-                    new_branch_to_checkout=dist_git_branch_name
-                )
-
-                transformator.create_archive()
-                transformator.copy_synced_content_to_dest_dir(
-                    synced_files=package_config.synced_files
-                )
-                transformator.add_patches_to_specfile()
-
-                commits = transformator.get_commits_to_upstream(upstream=target_ref)
-                commits_nice_str = commits_to_nice_str(commits)
 
-                logger.debug(f"Commits in source-git PR:\n{commits_nice_str}")
+        sourcegit = LocalProject(
+            git_url=target_url, working_dir=repo_directory, full_name=full_name
+        )
+
+        distgit = LocalProject(
+            git_url=package_config.metadata["dist_git_url"],
+            branch=f"source-git-{pr_id}",
+            git_service=PagureService(token=self.pagure_fork_token),
+            namespace="rpms",
+            repo_name=package_config.metadata["package_name"],
+        )
+
+        checkout_pr(repo=sourcegit.git_repo, pr_id=pr_id)
+
+        with Transformator(
+                sourcegit=sourcegit, distgit=distgit, package_config=package_config
+        ) as transformator:
+            transformator.create_archive()
+            transformator.copy_synced_content_to_distgit_directory(
+                synced_files=package_config.synced_files
+            )
+            transformator.add_patches_to_specfile()
 
-                msg = f"upstream commit: {top_commit}\n\nupstream repo: {target_url}"
-                transformator.commit_distgit(title=title, msg=msg)
+            commits = transformator.get_commits_to_upstream(upstream=target_ref)
+            commits_nice_str = commits_to_nice_str(commits)
 
-                package_name = package_config.metadata["package_name"]
-                pagure = PagureService(token=self.pagure_fork_token)
+            logger.debug(f"Commits in source-git PR:\n{commits_nice_str}")
 
-                project = pagure.get_project(repo=package_name, namespace="rpms")
+            msg = f"upstream commit: {top_commit}\n\nupstream repo: {target_url}"
+            transformator.commit_distgit(title=title, msg=msg)
 
-                project_fork = project.get_fork()
-                if not project_fork:
-                    logger.info("Creating a fork.")
-                    project.fork_create()
-                    project_fork = project.get_fork()
+            project_fork = distgit.git_project.get_fork()
+            if not project_fork:
+                logger.info("Creating a fork.")
+                distgit.git_project.fork_create()
+                project_fork = distgit.git_project.get_fork()
 
-                transformator.push_to_distgit_fork(
-                    project_fork=project_fork, branch_name=dist_git_branch_name
-                )
+            transformator.push_to_distgit_fork(
+                project_fork=project_fork, branch_name=distgit.branch
+            )
 
-                transformator.reset_checks(
-                    full_name,
-                    pr_id,
-                    github_token=self.github_token,
-                    pagure_user_token=self.pagure_user_token,
-                )
-                transformator.update_or_create_dist_git_pr(
-                    project,
-                    pr_id,
-                    pr_url,
-                    top_commit,
-                    title,
-                    source_ref=dist_git_branch_name,
-                    pagure_fork_token=self.pagure_fork_token,
-                    pagure_package_token=self.pagure_package_token,
-                )
-        finally:
-            if not repo_directory:
-                shutil.rmtree(repo.working_dir)
+            transformator.reset_checks(
+                full_name,
+                pr_id,
+                github_token=self.github_token,
+                pagure_user_token=self.pagure_user_token,
+            )
+            transformator.update_or_create_dist_git_pr(
+                distgit.git_project,
+                pr_id,
+                pr_url,
+                top_commit,
+                title,
+                source_ref=distgit.branch,
+                pagure_fork_token=self.pagure_fork_token,
+                pagure_package_token=self.pagure_package_token,
+            )
 
     def __enter__(self) -> Synchronizer:
         return self
diff --git a/packit/transformator.py b/packit/transformator.py
index 24432770..d0047643 100644
--- a/packit/transformator.py
+++ b/packit/transformator.py
@@ -3,11 +3,9 @@ from __future__ import annotations
 import logging
 import os
 import shutil
-import tempfile
-# decorator for cached properties, in python >=3.8 use cached_property
 from distutils.dir_util import copy_tree
 from functools import lru_cache
-from typing import Optional, List, Tuple
+from typing import List, Tuple
 
 import git
 
@@ -15,6 +13,7 @@ from ogr.abstract import GitProject
 from packit.config import PackageConfig
 from packit.constants import DG_PR_COMMENT_KEY_SG_PR, DG_PR_COMMENT_KEY_SG_COMMIT
 from packit.downstream_checks import get_check_by_name
+from packit.local_project import LocalProject
 from packit.utils import get_rev_list_kwargs, FedPKG, run_command
 from packit.watcher import SourceGitCheckHelper
 
@@ -29,27 +28,19 @@ class Transformator:
     """
 
     def __init__(
-            self,
-            package_config: PackageConfig,
-            branch: str = None,
-            version: str = None,
-            dest_dir: str = None,
-            fas_username: str = None,
-            url: str = None,
-            repo: git.Repo = None,
+        self,
+        package_config: PackageConfig,
+        sourcegit: LocalProject,
+        distgit: LocalProject,
+        version: str = None,
+        fas_username: str = None,
     ) -> None:
 
         self.package_config = package_config
-        self._repo = repo
-        if repo:
-            self._branch = branch or repo.active_branch
-            self.repo_url = url or list(repo.remote().urls)[0]
-        else:
-            self._branch = branch
-            self.repo_url = url
+        self.sourcegit = sourcegit
+        self.distgit = distgit
 
         self._version = version
-        self._temp_dir: Optional[str] = None
         self.rev_list_option_args = get_rev_list_kwargs(
             self.package_config.metadata.get("rev_list_option", ["first_parent"])
         )
@@ -58,60 +49,21 @@ class Transformator:
 
         self.upstream_name = self.package_config.metadata["upstream_name"]
         self.package_name = (
-                self.package_config.metadata["package_name"] or self.upstream_name
+            self.package_config.metadata["package_name"] or self.upstream_name
         )
-        self.dest_dir = dest_dir or tempfile.mkdtemp()
-        self.dist_git_url = self.package_config.metadata["dist_git_url"]
-
-    @property
-    def branch(self) -> str:
-        """
-        Source branch in the source-git repo.
-        """
-        if not self._branch:
-            self._branch = f"upstream-{self.version}"
-        return self._branch
-
-    @property
-    def repo(self) -> git.Repo:
-        """
-        Repository used as a source.
-        """
-        if not self._repo:
-            repo_path = os.path.join(self.temp_dir, self.upstream_name)
-            logger.info(
-                f"Cloning source-git repo: {self.repo_url} ({self.branch})-> {repo_path}"
-            )
-            self._repo = git.repo.Repo.clone_from(
-                url=self.repo_url, to_path=repo_path, branch=self.branch, tags=True
-            )
-        return self._repo
 
     @property
     @lru_cache()
-    def dist_git_repo(self) -> git.Repo:
-        """
-        Clone the dist_git repository to the destination dir and return git.Repo instance
-
-        :return: git.Repo instance
-        """
-        return self.clone_dist_git_repo()
-
-    @property
-    def temp_dir(self) -> str:
-        """
-        Dir used for storing temp. content. e.g. source-git repo.
-        """
-        if not self._temp_dir:
-            self._temp_dir = tempfile.mkdtemp()
-            logger.debug(f"Creating temp dir: {self._temp_dir}")
-        return self._temp_dir
+    def source_specfile_path(self) -> str:
+        return os.path.join(
+            self.sourcegit.working_dir, self.package_config.specfile_path
+        )
 
     @property
     @lru_cache()
-    def specfile_path(self) -> str:
+    def dist_specfile_path(self) -> str:
         return os.path.join(
-            self.repo.working_tree_dir, self.package_config.specfile_path
+            self.distgit.working_dir, f"{self.package_name}.spec"
         )
 
     @property
@@ -142,7 +94,7 @@ class Transformator:
                 "--qf",
                 "'%{version}\\n'",
                 "--srpm",
-                self.specfile_path,
+                self.source_specfile_path,
             ],
             output=True,
             fail=True,
@@ -158,20 +110,18 @@ class Transformator:
         """
         return FedPKG(
             fas_username=self.fas_username,
-            repo_path=self.dist_git_url,
-            directory=self.dest_dir,
+            repo_path=self.distgit.git_url,
+            directory=self.distgit.working_dir,
         )
 
     def clean(self) -> None:
         """
         Clean te temporary dir.
         """
-        logger.debug(f"Cleaning: {self.temp_dir}")
-        shutil.rmtree(self.temp_dir)
-        self._temp_dir = None
+        pass
 
     def create_archive(
-            self, path: str = None, name="{project}-{version}.tar.gz"
+        self, path: str = None, name="{project}-{version}.tar.gz"
     ) -> str:
         """
         Create archive from the provided git repo. The archive needs to be able to be used to build
@@ -181,13 +131,13 @@ class Transformator:
         :return: str, path to the archive
         """
         archive_name = name.format(project=self.upstream_name, version=self.version)
-        archive_path = path or os.path.join(self.dest_dir, archive_name)
+        archive_path = path or os.path.join(self.distgit.working_dir, archive_name)
 
         self.add_exclude_redhat_to_gitattributes()
 
         with open(archive_path, "wb") as fp:
-            self.repo.archive(
-                fp, prefix=f"./{self.upstream_name}/", worktree_attributes=True
+            self.sourcegit.git_repo.archive(
+                fp, prefix=f"./{self.upstream_name}-{self.version}/", worktree_attributes=True
             )
 
         logger.info(f"Archive created: {archive_path}")
@@ -199,44 +149,47 @@ class Transformator:
         TODO: We need to use upstream release archive directly
         """
         logger.debug("Adding 'redhat/ export-ignore' to .gitattributes")
-        gitattributes_path = os.path.join(self.repo.working_tree_dir, ".gitattributes")
+        gitattributes_path = os.path.join(self.sourcegit.working_dir, ".gitattributes")
         with open(gitattributes_path, "a") as gitattributes_file:
             for file in self.package_config.synced_files:
-                file_in_working_dir = os.path.join(self.repo.working_tree_dir, file)
+                file_in_working_dir = os.path.join(self.sourcegit.working_dir, file)
                 if os.path.isdir(file_in_working_dir):
                     gitattributes_file.writelines([f"{file}/ export-ignore\n"])
                 elif os.path.isfile(file_in_working_dir):
                     gitattributes_file.writelines([f"{file} export-ignore\n"])
 
     @lru_cache()
-    def create_srpm(self) -> None:
-        logger.debug("Start creating og the SRPM.")
+    def create_srpm(self) -> str:
+        logger.debug("Start creating of the SRPM.")
         archive = self.create_archive()
         logger.debug(f"Using archive: {archive}")
 
-        spec_dir = os.path.dirname(self.specfile_path)
-        run_command(
+        output = run_command(
             cmd=[
                 "rpmbuild",
                 "-bs",
-                f"{self.specfile_path}",
+                f"{self.dist_specfile_path}",
                 "--define",
-                f"_sourcedir {spec_dir}",
+                f"_sourcedir {self.distgit.working_dir}",
                 "--define",
-                f"_specdir {spec_dir}",
+                f"_specdir {self.distgit.working_dir}",
                 "--define",
-                f"_buildir {spec_dir}",
+                f"_buildir {self.distgit.working_dir}",
                 "--define",
-                f"_srcrpmdir {self.dest_dir}",
+                f"_srcrpmdir {self.distgit.working_dir}",
                 "--define",
-                f"_rpmdir {spec_dir}",
+                f"_rpmdir {self.distgit.working_dir}",
             ],
             fail=True,
+            output=True,
         )
+        specfile_name = output.split(':')[1].rstrip()
+        logger.info(f"Specfile created: {specfile_name}")
+        return specfile_name
 
     @lru_cache()
     def get_commits_to_upstream(
-            self, upstream: str, add_usptream_head_commit=False
+        self, upstream: str, add_usptream_head_commit=False
     ) -> List[git.Commit]:
         """
         Return the list of commits from current branch to upstream rev/tag.
@@ -245,49 +198,29 @@ class Transformator:
         :return: list of commits (last commit on the current branch.).
         """
 
-        if upstream in self.repo.tags:
+        if upstream in self.sourcegit.git_repo.tags:
             upstream_ref = upstream
         else:
             upstream_ref = f"origin/{upstream}"
-            if upstream_ref not in self.repo.refs:
+            if upstream_ref not in self.sourcegit.git_repo.refs:
                 raise Exception(
                     f"Upstream {upstream_ref} branch nor {upstream} tag not found."
                 )
 
         commits = list(
-            self.repo.iter_commits(
-                rev=f"{upstream_ref}..{self.branch}",
+            self.sourcegit.git_repo.iter_commits(
+                rev=f"{upstream_ref}..{self.sourcegit._branch}",
                 reverse=True,
                 **self.rev_list_option_args,
             )
         )
         if add_usptream_head_commit:
-            commits.insert(0, self.repo.refs[f"{upstream_ref}"].commit)
-
-        logger.debug(f"Delta ({upstream_ref}..{self.branch}): {len(commits)}")
-        return commits
-
-    @lru_cache()
-    def clone_dist_git_repo(self, new_branch_to_checkout: str = None) -> git.Repo:
-        """
-        Clone the dist_git repository to the destination dir and return git.Repo instance
+            commits.insert(0, self.sourcegit.git_repo.refs[f"{upstream_ref}"].commit)
 
-        :return: git.Repo instance
-        """
-        # TODO: optimize cloning: single branch and last 3 commits?
-        if os.path.isdir(os.path.join(self.dest_dir, ".git")):
-            logger.info(f"Dist-git already present: {self.dest_dir}")
-            return git.repo.Repo(self.dest_dir)
-
-        logger.info(f"Cloning dist-git repo: {self.dist_git_url} -> {self.dest_dir}")
-        repo = git.repo.Repo.clone_from(
-            url=self.dist_git_url, to_path=self.dest_dir, tags=True
+        logger.debug(
+            f"Delta ({upstream_ref}..{self.sourcegit._branch}): {len(commits)}"
         )
-
-        if new_branch_to_checkout:
-            new_branch = repo.create_head(new_branch_to_checkout)
-            new_branch.checkout()
-        return repo
+        return commits
 
     def create_patches(self, upstream: str = None) -> List[Tuple[str, str]]:
         """
@@ -304,7 +237,7 @@ class Transformator:
             parent = commits[i]
 
             patch_name = f"{i + 1:04d}-{commit.hexsha}.patch"
-            patch_path = os.path.join(self.dest_dir, patch_name)
+            patch_path = os.path.join(self.distgit.working_dir, patch_name)
             patch_msg = f"{commit.summary}\nAuthor: {commit.author.name} <{commit.author.email}>"
 
             logger.debug(f"PATCH: {patch_name}\n{patch_msg}")
@@ -319,7 +252,7 @@ class Transformator:
                     ".",
                     '":(exclude)redhat"',
                 ],
-                cwd=self.repo.working_tree_dir,
+                cwd=self.sourcegit.working_dir,
                 output=True,
             )
 
@@ -338,7 +271,12 @@ class Transformator:
         if patch_list is None:
             patch_list = self.create_patches()
 
-        specfile_path = os.path.join(self.dest_dir, f"{self.package_name}.spec")
+        if not patch_list:
+            return
+
+        specfile_path = os.path.join(
+            self.distgit.working_dir, f"{self.package_name}.spec"
+        )
 
         with open(file=specfile_path, mode="r+") as spec_file:
             last_source_position = None
@@ -367,19 +305,21 @@ class Transformator:
         logger.info(
             f"Patches ({len(patch_list)}) added to the specfile ({specfile_path})"
         )
-        self.repo.index.write()
+        self.sourcegit.git_repo.index.write()
 
-    def copy_synced_content_to_dest_dir(self, synced_files: List[str]) -> None:
+    def copy_synced_content_to_distgit_directory(self, synced_files: List[str]) -> None:
         """
         Copy files from source-git to destination directory.
         """
         for file in synced_files:
-            file_in_working_dir = os.path.join(self.repo.working_tree_dir, file)
+            file_in_working_dir = os.path.join(self.sourcegit.working_dir, file)
             logger.debug(f"Copying '{file_in_working_dir}' to distgit.")
             if os.path.isdir(file_in_working_dir):
-                copy_tree(src=file_in_working_dir, dst=self.dest_dir)
+                copy_tree(src=file_in_working_dir, dst=self.distgit.working_dir)
             elif os.path.isfile(file_in_working_dir):
-                shutil.copy2(file_in_working_dir, os.path.join(self.dest_dir, file))
+                shutil.copy2(
+                    file_in_working_dir, os.path.join(self.distgit.working_dir, file)
+                )
 
     def upload_archive_to_lookaside_cache(self, keytab: str) -> None:
         """
@@ -392,15 +332,15 @@ class Transformator:
 
     def commit_distgit(self, title: str, msg: str) -> None:
         main_msg = f"[source-git] {title}"
-        self.dist_git_repo.git.add("-A")
-        self.dist_git_repo.index.write()
+        self.distgit.git_repo.git.add("-A")
+        self.distgit.git_repo.index.write()
         # TODO: implement signing properly: we need to create a cert for the bot, distribute it to the container,
         #       prepare git config and then we can start signing
         # TODO: make -s configurable
-        self.dist_git_repo.git.commit("-s", "-m", main_msg, "-m", msg)
+        self.distgit.git_repo.git.commit("-s", "-m", main_msg, "-m", msg)
 
     def reset_checks(
-            self, full_name: str, pr_id: int, github_token: str, pagure_user_token: str
+        self, full_name: str, pr_id: int, github_token: str, pagure_user_token: str
     ) -> None:
         """
         Before syncing a new change downstream, we need to reset status of checks
@@ -413,15 +353,15 @@ class Transformator:
             sg.set_init_check(full_name, pr_id, check)
 
     def update_or_create_dist_git_pr(
-            self,
-            project: GitProject,
-            pr_id: int,
-            pr_url: str,
-            top_commit: str,
-            title: str,
-            source_ref: str,
-            pagure_fork_token: str,
-            pagure_package_token: str,
+        self,
+        project: GitProject,
+        pr_id: int,
+        pr_url: str,
+        top_commit: str,
+        title: str,
+        source_ref: str,
+        pagure_fork_token: str,
+        pagure_package_token: str,
     ) -> None:
         # Sadly, pagure does not support editing initial comments of a PR via the API
         # https://pagure.io/pagure/issue/4111
@@ -468,7 +408,7 @@ class Transformator:
             # FIXME: consider storing the data above as a git note of the top commit
             project.change_token(pagure_package_token)
             project.pr_comment(pr.id, msg)
-            logger.info("new comment added on PR %s", sg_pr_id)
+            logger.info(f"new comment added on PR {pr.id} ({pr.url})")
             break
         else:
             logger.debug(f"Matching dist-git PR not found => creating a new one.")
@@ -490,13 +430,15 @@ class Transformator:
             logger.info(f"PR created: {dist_git_pr_id}")
 
     def push_to_distgit_fork(self, project_fork, branch_name):
-        if "origin-fork" not in [remote.name for remote in self.dist_git_repo.remotes]:
-            self.dist_git_repo.create_remote(
+        if "origin-fork" not in [
+            remote.name for remote in self.distgit.git_repo.remotes
+        ]:
+            self.distgit.git_repo.create_remote(
                 name="origin-fork", url=project_fork.get_git_urls()["ssh"]
             )
 
         # I suggest to comment this one while testing when the push is not needed
-        self.dist_git_repo.remote("origin-fork").push(
+        self.distgit.git_repo.remote("origin-fork").push(
             refspec=branch_name, force=branch_name in project_fork.get_branches()
         )
 
diff --git a/packit/utils.py b/packit/utils.py
index dfaa9246..67874fbc 100644
--- a/packit/utils.py
+++ b/packit/utils.py
@@ -7,7 +7,7 @@ import tempfile
 
 import git
 
-logger = logging.getLogger("source_git")
+logger = logging.getLogger(__file__)
 
 
 def get_rev_list_kwargs(opt_list):


Test patch:
diff --git a/tests/unit/test_local_project.py b/tests/unit/test_local_project.py
new file mode 100644
index 00000000..c2102592
--- /dev/null
+++ b/tests/unit/test_local_project.py
@@ -0,0 +1,183 @@
+import git
+from flexmock import flexmock
+
+from packit import local_project
+from packit.local_project import LocalProject
+
+
+def test_local_project_full_name():
+    project = LocalProject(full_name="namespace/repository_name")
+    assert project.repo_name == "repository_name"
+    assert project.namespace == "namespace"
+    assert project.full_name == "namespace/repository_name"
+
+
+def test_local_project_repo_namespace():
+    project = LocalProject(repo_name="repository_name", namespace="namespace")
+    assert project.repo_name == "repository_name"
+    assert project.namespace == "namespace"
+    assert project.full_name == "namespace/repository_name"
+
+
+def test_local_project_working_dir_project():
+    """Get git_url, namespace/repo and service from git_project"""
+    project_mock = flexmock(
+        repo="repository_name",
+        namespace="namespace",
+        service=flexmock(),
+        get_git_urls=lambda: {"git": "ssh url"},
+    )
+
+    project = LocalProject(
+        git_project=project_mock,
+        working_dir=flexmock(),
+        git_repo=flexmock(active_branch="branch"),
+    )
+    assert project.git_service
+    assert project.git_project
+    assert project.repo_name == "repository_name"
+    assert project.namespace == "namespace"
+    assert project.full_name == "namespace/repository_name"
+
+
+def test_local_project_repo_url():
+    """Get working_dir from git_repo"""
+    project = LocalProject(
+        git_repo=flexmock(active_branch="branch", working_dir="something"),
+        git_url=flexmock(),
+    )
+    assert project.git_repo
+    assert project.working_dir == "something"
+    assert project._branch == "branch"
+
+
+def test_local_project_repo():
+    """Get git_url from git_repo"""
+    project = LocalProject(
+        git_repo=flexmock(active_branch="branch", working_dir="something")
+        .should_receive("remote")
+        .replace_with(lambda: flexmock(urls=["git/url"]))
+        .once()
+        .mock()
+    )
+    assert project.git_repo
+    assert project.working_dir == "something"
+    assert project._branch == "branch"
+    assert project.git_url == "git/url"
+
+
+def test_clone_project_checkout_branch():
+    """Checkout existing branch"""
+    project = LocalProject(
+        git_repo=flexmock(
+            active_branch="branch",
+            working_dir="something",
+            branches={
+                "other": flexmock(checkout=lambda: None)
+                .should_receive("checkout")
+                .once()
+                .mock()
+            },
+        ),
+        branch="other",
+        git_url=flexmock(),
+    )
+    assert project.git_repo
+    assert project.working_dir == "something"
+    assert project._branch == "other"
+
+
+def test_clone_project_checkout_new_branch():
+    """Checkout newly created branch"""
+    branches = {}
+    project = LocalProject(
+        git_repo=flexmock(
+            active_branch="branch", working_dir="something", branches=branches
+        )
+        .should_receive("create_head")
+        .with_args("other")
+        .replace_with(
+            lambda x: branches.setdefault(
+                x, flexmock().should_receive("checkout").once().mock()
+            )
+        )
+        .once()
+        .mock(),
+        branch="other",
+        git_url=flexmock(),
+    )
+    assert project.git_repo
+    assert project.working_dir == "something"
+    assert project._branch == "other"
+
+
+def test_clone_project_service_repo_namespace():
+    """Get git_project from git_service and namespace/repo"""
+    project = LocalProject(
+        repo_name="repo",
+        namespace="namespace",
+        git_service=flexmock()
+        .should_receive("get_project")
+        .with_args(repo="repo", namespace="namespace")
+        .replace_with(lambda repo, namespace: flexmock())
+        .mock(),
+        git_url=flexmock(),
+        working_dir=flexmock(),
+        git_repo=flexmock(active_branch=flexmock()),
+    )
+    assert project.repo_name
+    assert project.namespace
+    assert project.git_service
+    assert project.git_project
+
+
+def test_local_project_clone():
+    flexmock(local_project).should_receive("get_repo").with_args(
+        "http://some.example/url"
+    ).and_return(flexmock(working_dir="some/example/path", active_branch="branch"))
+
+    project = LocalProject(git_url="http://some.example/url")
+
+    assert project.git_url
+    assert project.git_repo
+    assert project.branch == "branch"
+    assert project.working_dir_temporary
+
+    project.working_dir_temporary = False
+
+
+def test_local_project_repo_from_working_dir():
+    flexmock(local_project).should_receive("is_git_repo").with_args(
+        "some/example/path"
+    ).and_return(True)
+    flexmock(
+        git,
+        Repo=flexmock(
+            active_branch="branch", remote=lambda: flexmock(urls=["git/url"])
+        ),
+    )
+    project = LocalProject(working_dir="some/example/path")
+
+    assert project.git_url == "git/url"
+    assert project.git_repo
+    assert project.git_repo.active_branch == "branch"
+    assert project.branch == "branch"
+    assert not project.working_dir_temporary
+
+
+def test_local_project_dir_url():
+
+    flexmock(local_project).should_receive("get_repo").with_args(
+        "http://some.example/url", "some/example/path"
+    ).and_return(flexmock(working_dir="some/example/path", active_branch="branch"))
+
+    project = LocalProject(
+        git_url="http://some.example/url", working_dir="some/example/path"
+    )
+
+    assert project.git_url == "http://some.example/url"
+    assert project.git_repo
+    assert project.branch == "branch"
+    assert project.git_repo.active_branch == "branch"
+    assert project.git_repo.working_dir == "some/example/path"
+    assert not project.working_dir_temporary