From afad331698e8f113ad768a5fd87010087256a904 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Sun, 5 Nov 2023 12:23:41 -0500 Subject: [PATCH] Update the origin remote URL for a cached template before using it --- changes/1158.bugfix.rst | 1 + src/briefcase/commands/base.py | 4 + .../base/test_update_cookiecutter_cache.py | 79 +++++++++++++++++-- 3 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 changes/1158.bugfix.rst diff --git a/changes/1158.bugfix.rst b/changes/1158.bugfix.rst new file mode 100644 index 000000000..b7e0955b7 --- /dev/null +++ b/changes/1158.bugfix.rst @@ -0,0 +1 @@ +When a custom Briefcase template from a git repository is used to create an app, Briefcase now ensures that git repository is always used. diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 06fe41316..2f87eed9d 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -844,6 +844,10 @@ def update_cookiecutter_cache(self, template: str, branch="master"): repo = self.tools.git.Repo(cached_template) # Raises ValueError if "origin" isn't a valid remote remote = repo.remote(name="origin") + # Ensure the existing repo's origin URL points to the location + # being requested. A difference can occur, for instance, if a + # fork of the template is used. + remote.set_url(new_url=template, old_url=remote.url) try: # Attempt to update the repository remote.fetch() diff --git a/tests/commands/base/test_update_cookiecutter_cache.py b/tests/commands/base/test_update_cookiecutter_cache.py index cf4de856f..61266279a 100644 --- a/tests/commands/base/test_update_cookiecutter_cache.py +++ b/tests/commands/base/test_update_cookiecutter_cache.py @@ -84,6 +84,7 @@ def test_explicit_cached_repo_template(base_command, mock_git): base_command.tools.git.Repo.return_value = mock_repo mock_repo.remote.return_value = mock_remote mock_remote.refs.__getitem__.return_value = mock_remote_head + mock_remote.url = "https://example.com/magic/special-template.git" cached_path = cookiecutter_cache_path( "https://example.com/magic/special-template.git" @@ -98,8 +99,59 @@ def test_explicit_cached_repo_template(base_command, mock_git): # The cookiecutter cache location will be interrogated. base_command.tools.git.Repo.assert_called_once_with(cached_path) - # The origin of the repo was fetched + # The origin of the repo was updated and fetched mock_repo.remote.assert_called_once_with(name="origin") + mock_remote.set_url.assert_called_once_with( + new_url="https://example.com/magic/special-template.git", + old_url="https://example.com/magic/special-template.git", + ) + mock_remote.fetch.assert_called_once_with() + + # The right branch was accessed + mock_remote.refs.__getitem__.assert_called_once_with("special") + + # The remote head was checked out. + mock_remote_head.checkout.assert_called_once_with() + + # The template that will be used is the original URL + assert cached_template == cached_path + + +def test_explicit_cached_repo_template_with_diff_url(base_command, mock_git): + """If a previously known URL template is specified but uses a different remote URL, + the repo's origin URL is updated and is used.""" + base_command.tools.git = mock_git + + mock_repo = mock.MagicMock() + mock_remote = mock.MagicMock() + mock_remote_head = mock.MagicMock() + + # Git returns a Repo, that repo can return a remote, and it has + # heads that can be accessed. + base_command.tools.git.Repo.return_value = mock_repo + mock_repo.remote.return_value = mock_remote + mock_remote.refs.__getitem__.return_value = mock_remote_head + mock_remote.url = "https://example.com/existing/special-template.git" + + cached_path = cookiecutter_cache_path( + "https://example.com/magic/special-template.git" + ) + + # Update the cache + cached_template = base_command.update_cookiecutter_cache( + template="https://example.com/magic/special-template.git", + branch="special", + ) + + # The cookiecutter cache location will be interrogated. + base_command.tools.git.Repo.assert_called_once_with(cached_path) + + # The origin of the repo was updated and fetched + mock_repo.remote.assert_called_once_with(name="origin") + mock_remote.set_url.assert_called_once_with( + new_url="https://example.com/magic/special-template.git", + old_url="https://example.com/existing/special-template.git", + ) mock_remote.fetch.assert_called_once_with() # The right branch was accessed @@ -126,6 +178,7 @@ def test_offline_repo_template(base_command, mock_git): # will cause a git error (error code 128). base_command.tools.git.Repo.return_value = mock_repo mock_repo.remote.return_value = mock_remote + mock_remote.url = "https://example.com/magic/special-template.git" mock_remote.refs.__getitem__.return_value = mock_remote_head mock_remote.fetch.side_effect = git_exceptions.GitCommandError("git", 128) @@ -141,8 +194,12 @@ def test_offline_repo_template(base_command, mock_git): # The cookiecutter cache location will be interrogated. base_command.tools.git.Repo.assert_called_once_with(cached_path) - # The origin of the repo was fetched + # The origin of the repo was updated and fetched mock_repo.remote.assert_called_once_with(name="origin") + mock_remote.set_url.assert_called_once_with( + new_url="https://example.com/magic/special-template.git", + old_url="https://example.com/magic/special-template.git", + ) mock_remote.fetch.assert_called_once_with() # The right branch was accessed @@ -167,6 +224,7 @@ def test_cached_missing_branch_template(base_command, mock_git): # raises an IndexError base_command.tools.git.Repo.return_value = mock_repo mock_repo.remote.return_value = mock_remote + mock_remote.url = "https://example.com/magic/special-template.git" mock_remote.refs.__getitem__.side_effect = IndexError cached_path = cookiecutter_cache_path( @@ -183,23 +241,27 @@ def test_cached_missing_branch_template(base_command, mock_git): # The cookiecutter cache location will be interrogated. base_command.tools.git.Repo.assert_called_once_with(cached_path) - # The origin of the repo was fetched + # The origin of the repo was updated and fetched mock_repo.remote.assert_called_once_with(name="origin") + mock_remote.set_url.assert_called_once_with( + new_url="https://example.com/magic/special-template.git", + old_url="https://example.com/magic/special-template.git", + ) mock_remote.fetch.assert_called_once_with() # An attempt to access the branch was made mock_remote.refs.__getitem__.assert_called_once_with("invalid") -def test_value_error(base_command, mock_git): - """If the git clone fails a ValueError is raised.""" +def test_git_repo_with_missing_origin_remote(base_command, mock_git): + """If the local git repo doesn't have an origin remote, a ValueError is raised.""" base_command.tools.git = mock_git mock_repo = mock.MagicMock() mock_remote = mock.MagicMock() - # Git returns a Repo, that repo can return a remote, and it has - # heads that can be accessed. However, getting the remote will fail if git clone is not complete. + # Git returns a Repo, that repo can return a remote, and it has heads that can be + # accessed. However, getting the remote will fail if git clone is not complete. base_command.tools.git.Repo.return_value = mock_repo mock_repo.remote.side_effect = ValueError("Remote named origin did not exist") @@ -216,6 +278,7 @@ def test_value_error(base_command, mock_git): # The cookiecutter cache location will be interrogated. base_command.tools.git.Repo.assert_called_once_with(cached_path) - # The origin of the repo was fetched + # The origin of the repo was not updated or fetched mock_repo.remote.assert_called_once_with(name="origin") + mock_remote.set_url.assert_not_called() mock_remote.fetch.assert_not_called()