diff --git a/docs/configuration.rst b/docs/configuration.rst index ccc60350..851e1416 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -104,6 +104,7 @@ It supports authorization; to do so you'd need to prefix the url with authorizat Remote push trigger ^^^^^^^^^^^^^^^^^^^ +* ``commit_author`` - git commit author, string, optional. In case if not set, the git will generate author for you. Note, however, that in this case it will disclosure your hostname. * ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required. * ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``. diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 620256c2..784e79d5 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -161,12 +161,12 @@ class Sources(LazyLogging): str: patch as plain text """ instance = Sources() - instance.add(sources_dir, *pattern) + instance.add(sources_dir, *pattern, intent_to_add=True) diff = instance.diff(sources_dir) return f"{diff}\n" # otherwise, patch will be broken @staticmethod - def push(sources_dir: Path, remote: RemoteSource, *pattern: str) -> None: + def push(sources_dir: Path, remote: RemoteSource, *pattern: str, commit_author: Optional[str] = None) -> None: """ commit selected changes and push files to the remote repository @@ -174,19 +174,21 @@ class Sources(LazyLogging): sources_dir(Path): local path to git repository remote(RemoteSource): remote target, branch and url *pattern(str): glob patterns + commit_author(Optional[str]): commit author in form of git config (i.e. ``user ``) """ instance = Sources() instance.add(sources_dir, *pattern) - instance.commit(sources_dir) + instance.commit(sources_dir, author=commit_author) Sources._check_output("git", "push", remote.git_url, remote.branch, cwd=sources_dir, logger=instance.logger) - def add(self, sources_dir: Path, *pattern: str) -> None: + def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None: """ track found files via git Args: sources_dir(Path): local path to git repository *pattern(str): glob patterns + intent_to_add(bool): record only the fact that it will be added later, acts as --intent-to-add git flag """ # glob directory to find files which match the specified patterns found_files: List[Path] = [] @@ -196,23 +198,26 @@ class Sources(LazyLogging): return # no additional files found self.logger.info("found matching files %s", found_files) # add them to index - Sources._check_output("git", "add", "--intent-to-add", - *[str(fn.relative_to(sources_dir)) for fn in found_files], + args = ["--intent-to-add"] if intent_to_add else [] + Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files], cwd=sources_dir, logger=self.logger) - def commit(self, sources_dir: Path, commit_message: Optional[str] = None) -> None: + def commit(self, sources_dir: Path, message: Optional[str] = None, author: Optional[str] = None) -> None: """ commit changes Args: sources_dir(Path): local path to git repository - commit_message(Optional[str]): optional commit message if any. If none set, message will be generated - according to the current timestamp + message(Optional[str]): optional commit message if any. If none set, message will be generated according to + the current timestamp + author(Optional[str]): optional commit author if any """ - if commit_message is None: - commit_message = f"Autogenerated commit at {datetime.datetime.utcnow()}" - Sources._check_output("git", "commit", "--allow-empty", "--message", commit_message, - cwd=sources_dir, logger=self.logger) + if message is None: + message = f"Autogenerated commit at {datetime.datetime.utcnow()}" + args = ["--allow-empty", "--message", message] + if author is not None: + args.extend(["--author", author]) + Sources._check_output("git", "commit", *args, cwd=sources_dir, logger=self.logger) def diff(self, sources_dir: Path) -> str: """ diff --git a/src/ahriman/core/gitremote/remote_push.py b/src/ahriman/core/gitremote/remote_push.py index eac3ecac..9f0b03ff 100644 --- a/src/ahriman/core/gitremote/remote_push.py +++ b/src/ahriman/core/gitremote/remote_push.py @@ -38,6 +38,7 @@ class RemotePush(LazyLogging): sync PKGBUILDs to remote repository after actions Attributes: + commit_author(Optional[str]): optional commit author in form of git config (i.e. ``user ``) remote_source(RemoteSource): repository remote source (remote pull url and branch) """ @@ -49,6 +50,7 @@ class RemotePush(LazyLogging): configuration(Configuration): configuration instance remote_push_trigger.py """ + self.commit_author = configuration.get(section, "commit_author", fallback=None) self.remote_source = RemoteSource( git_url=configuration.get(section, "push_url"), web_url="", @@ -73,10 +75,8 @@ class RemotePush(LazyLogging): # 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) + # ...secondly, we clone whole tree... + Sources.fetch(package_target_dir, package.remote) # ...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 @@ -107,7 +107,8 @@ class RemotePush(LazyLogging): 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)) + Sources.push(clone_dir, self.remote_source, *RemotePush.packages_update(result, clone_dir), + commit_author=self.commit_author) except Exception: self.logger.exception("git push failed") raise GitRemoteError() diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index f4d5b3bc..09a6bc00 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -193,7 +193,7 @@ def test_patch_create(mocker: MockerFixture) -> None: diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff") Sources.patch_create(Path("local"), "glob") - add_mock.assert_called_once_with(Path("local"), "glob") + add_mock.assert_called_once_with(Path("local"), "glob", intent_to_add=True) diff_mock.assert_called_once_with(Path("local")) @@ -214,10 +214,11 @@ def test_push(package_ahriman: Package, mocker: MockerFixture) -> None: commit_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.commit") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + author = "commit author " local = Path("local") - Sources.push(Path("local"), package_ahriman.remote, "glob") + Sources.push(Path("local"), package_ahriman.remote, "glob", commit_author=author) add_mock.assert_called_once_with(local, "glob") - commit_mock.assert_called_once_with(local) + commit_mock.assert_called_once_with(local, author=author) check_output_mock.assert_called_once_with( "git", "push", package_ahriman.remote.git_url, package_ahriman.remote.branch, cwd=local, logger=pytest.helpers.anyvar(int)) @@ -233,6 +234,21 @@ def test_add(sources: Sources, mocker: MockerFixture) -> None: local = Path("local") sources.add(local, "pattern1", "pattern2") glob_mock.assert_has_calls([MockCall("pattern1"), MockCall("pattern2")]) + check_output_mock.assert_called_once_with( + "git", "add", "1", "2", "1", "2", cwd=local, logger=pytest.helpers.anyvar(int) + ) + + +def test_add_intent_to_add(sources: Sources, mocker: MockerFixture) -> None: + """ + must add files to git with --intent-to-add flag + """ + glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("local/1"), Path("local/2")]) + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + local = Path("local") + sources.add(local, "pattern1", "pattern2", intent_to_add=True) + glob_mock.assert_has_calls([MockCall("pattern1"), MockCall("pattern2")]) check_output_mock.assert_called_once_with( "git", "add", "--intent-to-add", "1", "2", "1", "2", cwd=local, logger=pytest.helpers.anyvar(int) ) @@ -256,14 +272,30 @@ def test_commit(sources: Sources, mocker: MockerFixture) -> None: check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") - commit_message = "Commit message" - sources.commit(local, commit_message=commit_message) + message = "Commit message" + sources.commit(local, message=message) check_output_mock.assert_called_once_with( - "git", "commit", "--allow-empty", "--message", commit_message, cwd=local, logger=pytest.helpers.anyvar(int) + "git", "commit", "--allow-empty", "--message", message, cwd=local, logger=pytest.helpers.anyvar(int) ) -def test_commit_autogenerated(sources: Sources, mocker: MockerFixture) -> None: +def test_commit_author(sources: Sources, mocker: MockerFixture) -> None: + """ + must commit changes with commit author + """ + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + local = Path("local") + message = "Commit message" + author = "commit author " + sources.commit(Path("local"), message=message, author=author) + check_output_mock.assert_called_once_with( + "git", "commit", "--allow-empty", "--message", message, "--author", author, + cwd=local, logger=pytest.helpers.anyvar(int) + ) + + +def test_commit_autogenerated_message(sources: Sources, mocker: MockerFixture) -> None: """ must commit changes with autogenerated commit message """ diff --git a/tests/ahriman/core/gitremote/test_remote_push.py b/tests/ahriman/core/gitremote/test_remote_push.py index 3fb4e318..8bdff757 100644 --- a/tests/ahriman/core/gitremote/test_remote_push.py +++ b/tests/ahriman/core/gitremote/test_remote_push.py @@ -17,17 +17,14 @@ def test_package_update(package_ahriman: Package, mocker: MockerFixture) -> None """ 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([ MockCall(local / package_ahriman.base, ignore_errors=True), - MockCall(pytest.helpers.anyvar(int), onerror=pytest.helpers.anyvar(int)), # removal of the TemporaryDirectory MockCall(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: @@ -53,7 +50,9 @@ def test_run(configuration: Configuration, result: Result, package_ahriman: Pack 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) + push_mock.assert_called_once_with( + pytest.helpers.anyvar(int), runner.remote_source, package_ahriman.base, commit_author=runner.commit_author + ) def test_run_failed(configuration: Configuration, result: Result, mocker: MockerFixture) -> None: diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index d33bf6f9..1118b86b 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -40,6 +40,7 @@ target = gitremote target = gitremote [gitremote] +commit_author = "user " push_url = https://github.com/arcan1s/repository.git pull_url = https://github.com/arcan1s/repository.git