change gitremote api to be same as report and upload

These changes are keeping fallback to old settings, but will allow to
run multiple git targets with different settings
This commit is contained in:
Evgenii Alekseev 2022-11-02 04:09:42 +02:00
parent 4f35ec6542
commit fb6b22cdd7
13 changed files with 399 additions and 193 deletions

View File

@ -4,6 +4,14 @@ ahriman.core.gitremote package
Submodules
----------
ahriman.core.gitremote.remote\_pull module
------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_pull
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.gitremote.remote\_pull\_trigger module
---------------------------------------------------
@ -12,6 +20,14 @@ ahriman.core.gitremote.remote\_pull\_trigger module
:no-undoc-members:
:show-inheritance:
ahriman.core.gitremote.remote\_push module
------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_push
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.gitremote.remote\_push\_trigger module
---------------------------------------------------

View File

@ -1,3 +1,4 @@
FAQ
===
@ -131,6 +132,9 @@ For that purpose you could use ``RemotePullTrigger`` trigger. To do so you will
.. code-block:: ini
[remote-pull]
target = gitremote
[gitremote]
pull_url = https://github.com/username/repository
@ -154,6 +158,9 @@ For that purpose you'd need to use another trigger called ``RemotePushTrigger``.
.. code-block:: ini
[remote-push]
target = gitremote
[gitremote]
push_url = https://github.com/username/repository

View File

@ -55,6 +55,18 @@ class ExitCode(RuntimeError):
"""
class GitRemoteFailed(RuntimeError):
"""
git remote exception
"""
def __init__(self) -> None:
"""
default constructor
"""
RuntimeError.__init__(self, "Git remote failed")
class InitializeException(RuntimeError):
"""
base service initialization exception

View File

@ -0,0 +1,90 @@
#
# copyright (c) 2021-2022 ahriman team.
#
# this file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu general public license as published by
# the free software foundation, either version 3 of the license, or
# (at your option) any later version.
#
# this program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose. see the
# gnu general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteFailed
from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import walk
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
class RemotePull(LazyLogging):
"""
fetch PKGBUILDs from remote repository and use them for following actions
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
repository_paths(RepositoryPaths): repository paths instance
"""
def __init__(self, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
section(str): settings section name
"""
self.remote_source = RemoteSource(
git_url=configuration.get(section, "pull_url"),
web_url="",
path=".",
branch=configuration.get(section, "pull_branch", fallback="master"),
source=PackageSource.Local,
)
self.repository_paths = configuration.repository_paths
def repo_clone(self) -> None:
"""
clone repository from remote source
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
self.repo_copy(clone_dir)
def repo_copy(self, clone_dir: Path) -> None:
"""
copy directories from cloned remote source to local cache
Args:
clone_dir(Path): path to temporary cloned directory
"""
for pkgbuild_path in filter(lambda path: path.name == "PKGBUILD", walk(clone_dir)):
cloned_pkgbuild_dir = pkgbuild_path.parent
package_base = cloned_pkgbuild_dir.name
local_pkgbuild_dir = self.repository_paths.cache_for(package_base)
shutil.copytree(cloned_pkgbuild_dir, local_pkgbuild_dir, dirs_exist_ok=True)
Sources.init(local_pkgbuild_dir) # initialized git repository is required for local sources
def run(self) -> None:
"""
run git pull action
"""
try:
self.repo_clone()
except Exception:
self.logger.exception("git pull failed")
raise GitRemoteFailed()

View File

@ -17,26 +17,14 @@
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote.remote_pull import RemotePull
from ahriman.core.triggers import Trigger
from ahriman.core.util import walk
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
class RemotePullTrigger(Trigger):
"""
trigger for fetching PKGBUILDs from remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
repository_paths(RepositoryPaths): repository paths instance
trigger based on pulling PKGBUILDs before the actions
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -48,39 +36,12 @@ class RemotePullTrigger(Trigger):
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "pull_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "pull_branch", fallback="master"),
source=PackageSource.Local,
)
self.repository_paths = configuration.repository_paths
self.targets = configuration.getlist("remote-pull", "target", fallback=["gitremote"])
def on_start(self) -> None:
"""
trigger action which will be called at the start of the application
"""
self.repo_clone()
def repo_clone(self) -> None:
"""
clone repository from remote source
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
self.repo_copy(clone_dir)
def repo_copy(self, clone_dir: Path) -> None:
"""
copy directories from cloned remote source to local cache
Args:
clone_dir(Path): path to temporary cloned directory
"""
for pkgbuild_path in filter(lambda path: path.name == "PKGBUILD", walk(clone_dir)):
cloned_pkgbuild_dir = pkgbuild_path.parent
package_base = cloned_pkgbuild_dir.name
local_pkgbuild_dir = self.repository_paths.cache_for(package_base)
shutil.copytree(cloned_pkgbuild_dir, local_pkgbuild_dir, dirs_exist_ok=True)
Sources.init(local_pkgbuild_dir) # initialized git repository is required for local sources
for target in self.targets:
runner = RemotePull(self.configuration, target)
runner.run()

View File

@ -0,0 +1,113 @@
#
# copyright (c) 2021-2022 ahriman team.
#
# this file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu general public license as published by
# the free software foundation, either version 3 of the license, or
# (at your option) any later version.
#
# this program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose. see the
# gnu general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Generator
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteFailed
from ahriman.core.lazy_logging import LazyLogging
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.result import Result
class RemotePush(LazyLogging):
"""
sync PKGBUILDs to remote repository after actions
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
def __init__(self, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
remote_push_trigger.py
"""
self.remote_source = RemoteSource(
git_url=configuration.get(section, "push_url"),
web_url="",
path=".",
branch=configuration.get(section, "push_branch", fallback="master"),
source=PackageSource.Local,
)
@staticmethod
def package_update(package: Package, target_dir: Path) -> str:
"""
clone specified package and update its content in cloned PKGBUILD repository
Args:
package(Package): built package to update pkgbuild repository
target_dir(Path): path to the cloned PKGBUILD repository
Returns:
str: relative path to be added as new file
"""
# instead of iterating by directory we can simplify the process
# firstly, we need to remove old data to make sure that removed files are not tracked anymore...
package_target_dir = target_dir / package.base
shutil.rmtree(package_target_dir, ignore_errors=True)
# ...secondly, we copy whole tree...
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, package.remote)
shutil.copytree(clone_dir, package_target_dir)
# ...and last, but not least, we remove the dot-git directory...
shutil.rmtree(package_target_dir / ".git", ignore_errors=True)
# ...and finally return path to the copied directory
return package.base
@staticmethod
def packages_update(result: Result, target_dir: Path) -> Generator[str, None, None]:
"""
update all packages from the build result
Args:
result(Result): build result
target_dir(Path): path to the cloned PKGBUILD repository
Yields:
str: path to updated files
"""
for package in result.success:
yield RemotePush.package_update(package, target_dir)
def run(self, result: Result) -> None:
"""
run git pull action
Args:
result(Result): build result
"""
try:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
Sources.push(clone_dir, self.remote_source, *RemotePush.packages_update(result, clone_dir))
except Exception:
self.logger.exception("git push failed")
raise GitRemoteFailed()

View File

@ -17,27 +17,18 @@
# you should have received a copy of the gnu general public license
# along with this program. if not, see <http://www.gnu.org/licenses/>.
#
import shutil
from typing import Iterable
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Generator, Iterable
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote.remote_push import RemotePush
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.result import Result
class RemotePushTrigger(Trigger):
"""
trigger for syncing PKGBUILDs to remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -49,53 +40,7 @@ class RemotePushTrigger(Trigger):
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "push_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "push_branch", fallback="master"),
source=PackageSource.Local,
)
@staticmethod
def package_update(package: Package, target_dir: Path) -> str:
"""
clone specified package and update its content in cloned PKGBUILD repository
Args:
package(Package): built package to update pkgbuild repository
target_dir(Path): path to the cloned PKGBUILD repository
Returns:
str: relative path to be added as new file
"""
# instead of iterating by directory we can simplify the process
# firstly, we need to remove old data to make sure that removed files are not tracked anymore...
package_target_dir = target_dir / package.base
shutil.rmtree(package_target_dir, ignore_errors=True)
# ...secondly, we copy whole tree...
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, package.remote)
shutil.copytree(clone_dir, package_target_dir)
# ...and last, but not least, we remove the dot-git directory...
shutil.rmtree(package_target_dir / ".git", ignore_errors=True)
# ...and finally return path to the copied directory
return package.base
@staticmethod
def packages_update(result: Result, target_dir: Path) -> Generator[str, None, None]:
"""
update all packages from the build result
Args:
result(Result): build result
target_dir(Path): path to the cloned PKGBUILD repository
Yields:
str: path to updated files
"""
for package in result.success:
yield RemotePushTrigger.package_update(package, target_dir)
self.targets = configuration.getlist("remote-push", "target", fallback=["gitremote"])
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
@ -105,6 +50,6 @@ class RemotePushTrigger(Trigger):
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
Sources.push(clone_dir, self.remote_source, *RemotePushTrigger.packages_update(result, clone_dir))
for target in self.targets:
runner = RemotePush(self.configuration, target)
runner.run(result)

View File

@ -0,0 +1,70 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteFailed
from ahriman.core.gitremote.remote_pull import RemotePull
def test_repo_clone(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repository locally and copy its content
"""
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copy_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_copy")
runner = RemotePull(configuration, "gitremote")
runner.repo_clone()
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source)
copy_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_repo_copy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must copy repository tree from temporary directory to the local cache
"""
mocker.patch("ahriman.core.gitremote.remote_pull.walk", return_value=[
Path("local") / "package1" / "PKGBUILD",
Path("local") / "package1" / ".SRCINFO",
Path("local") / "package2" / ".SRCINFO",
Path("local") / "package3" / "PKGBUILD",
Path("local") / "package3" / ".SRCINFO",
])
copytree_mock = mocker.patch("shutil.copytree")
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
runner = RemotePull(configuration, "gitremote")
runner.repo_copy(Path("local"))
copytree_mock.assert_has_calls([
mock.call(Path("local") / "package1", configuration.repository_paths.cache_for("package1"), dirs_exist_ok=True),
mock.call(Path("local") / "package3", configuration.repository_paths.cache_for("package3"), dirs_exist_ok=True),
])
init_mock.assert_has_calls([
mock.call(configuration.repository_paths.cache_for("package1")),
mock.call(configuration.repository_paths.cache_for("package3")),
])
def test_run(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repo on run
"""
clone_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_clone")
runner = RemotePull(configuration, "gitremote")
runner.run()
clone_mock.assert_called_once_with()
def test_run_failed(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must reraise exception on error occurred
"""
mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.repo_clone", side_effect=Exception())
runner = RemotePull(configuration, "gitremote")
with pytest.raises(GitRemoteFailed):
runner.run()

View File

@ -1,8 +1,4 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote import RemotePullTrigger
@ -12,47 +8,8 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repo on start
"""
clone_mock = mocker.patch("ahriman.core.gitremote.RemotePullTrigger.repo_clone")
run_mock = mocker.patch("ahriman.core.gitremote.remote_pull.RemotePull.run")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.on_start()
clone_mock.assert_called_once_with()
def test_repo_clone(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clone repository locally and copy its content
"""
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copy_mock = mocker.patch("ahriman.core.gitremote.RemotePullTrigger.repo_copy")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.repo_clone()
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source)
copy_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_repo_copy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must copy repository tree from temporary directory to the local cache
"""
mocker.patch("ahriman.core.gitremote.remote_pull_trigger.walk", return_value=[
Path("local") / "package1" / "PKGBUILD",
Path("local") / "package1" / ".SRCINFO",
Path("local") / "package2" / ".SRCINFO",
Path("local") / "package3" / "PKGBUILD",
Path("local") / "package3" / ".SRCINFO",
])
copytree_mock = mocker.patch("shutil.copytree")
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
trigger = RemotePullTrigger("x86_64", configuration)
trigger.repo_copy(Path("local"))
copytree_mock.assert_has_calls([
mock.call(Path("local") / "package1", configuration.repository_paths.cache_for("package1"), dirs_exist_ok=True),
mock.call(Path("local") / "package3", configuration.repository_paths.cache_for("package3"), dirs_exist_ok=True),
])
init_mock.assert_has_calls([
mock.call(configuration.repository_paths.cache_for("package1")),
mock.call(configuration.repository_paths.cache_for("package3")),
])
run_mock.assert_called_once_with()

View File

@ -0,0 +1,67 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import GitRemoteFailed
from ahriman.core.gitremote.remote_push import RemotePush
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_package_update(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update single package
"""
rmtree_mock = mocker.patch("shutil.rmtree")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copytree_mock = mocker.patch("shutil.copytree")
local = Path("local")
RemotePush.package_update(package_ahriman, local)
rmtree_mock.assert_has_calls([
mock.call(local / package_ahriman.base, ignore_errors=True),
mock.call(pytest.helpers.anyvar(int), onerror=pytest.helpers.anyvar(int)), # removal of the TemporaryDirectory
mock.call(local / package_ahriman.base / ".git", ignore_errors=True),
])
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.remote)
copytree_mock.assert_called_once_with(pytest.helpers.anyvar(int), local / package_ahriman.base)
def test_packages_update(result: Result, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate packages update
"""
update_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.package_update",
return_value=[package_ahriman.base])
local = Path("local")
assert list(RemotePush.packages_update(result, local))
update_mock.assert_called_once_with(package_ahriman, local)
def test_run(configuration: Configuration, result: Result, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must push changes on result
"""
mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.packages_update", return_value=[package_ahriman.base])
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push")
runner = RemotePush(configuration, "gitremote")
runner.run(result)
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source)
push_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source, package_ahriman.base)
def test_run_failed(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
"""
must reraise exception on error occurred
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception())
runner = RemotePush(configuration, "gitremote")
with pytest.raises(GitRemoteFailed):
runner.run(result)

View File

@ -1,8 +1,4 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.gitremote import RemotePushTrigger
@ -10,47 +6,13 @@ from ahriman.models.package import Package
from ahriman.models.result import Result
def test_package_update(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update single package
"""
rmtree_mock = mocker.patch("shutil.rmtree")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
copytree_mock = mocker.patch("shutil.copytree")
local = Path("local")
RemotePushTrigger.package_update(package_ahriman, local)
rmtree_mock.assert_has_calls([
mock.call(local / package_ahriman.base, ignore_errors=True),
mock.call(pytest.helpers.anyvar(int), onerror=pytest.helpers.anyvar(int)), # removal of the TemporaryDirectory
mock.call(local / package_ahriman.base / ".git", ignore_errors=True),
])
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.remote)
copytree_mock.assert_called_once_with(pytest.helpers.anyvar(int), local / package_ahriman.base)
def test_packages_update(result: Result, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate packages update
"""
update_mock = mocker.patch("ahriman.core.gitremote.RemotePushTrigger.package_update",
return_value=[package_ahriman.base])
local = Path("local")
assert list(RemotePushTrigger.packages_update(result, local))
update_mock.assert_called_once_with(package_ahriman, local)
def test_on_result(configuration: Configuration, result: Result, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must push changes on result
"""
mocker.patch("ahriman.core.gitremote.RemotePushTrigger.packages_update", return_value=[package_ahriman.base])
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push")
run_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.run")
trigger = RemotePushTrigger("x86_64", configuration)
trigger.on_result(result, [package_ahriman])
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source)
push_mock.assert_called_once_with(pytest.helpers.anyvar(int), trigger.remote_source, package_ahriman.base)
run_mock.assert_called_once_with(result)

View File

@ -15,7 +15,7 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) ->
"""
mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception())
with pytest.raises(ReportFailed):
Report.load("x86_64", configuration, "html").run([], Result())
Report.load("x86_64", configuration, "html").run(Result(), [])
def test_report_dummy(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:

View File

@ -31,9 +31,15 @@ root = ../../../
[sign]
target =
[remote-push]
target = gitremote
[remote-pull]
target = gitremote
[gitremote]
pull_url = https://github.com/arcan1s/repository.git
push_url = https://github.com/arcan1s/repository.git
pull_url = https://github.com/arcan1s/repository.git
[report]
target =