From 9deec1d5d3a0c2b9b6a657c06fb324237417a7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Thu, 21 Jan 2021 17:34:05 +0000 Subject: [PATCH 01/24] Cope with 'encoding: false' in Git commits. --- breezy/git/mapping.py | 2 ++ breezy/git/tests/test_mapping.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/breezy/git/mapping.py b/breezy/git/mapping.py index 95118c134f..2a8cc68f05 100644 --- a/breezy/git/mapping.py +++ b/breezy/git/mapping.py @@ -418,9 +418,11 @@ def decode_using_encoding(rev, commit, encoding): rev.properties[u'author'] = commit.author.decode(encoding) rev.message, rev.git_metadata = self._decode_commit_message( rev, commit.message, encoding) + if commit.encoding is not None: rev.properties[u'git-explicit-encoding'] = commit.encoding.decode( 'ascii') + if commit.encoding is not None and commit.encoding != b'false': decode_using_encoding(rev, commit, commit.encoding.decode('ascii')) else: for encoding in ('utf-8', 'latin1'): diff --git a/breezy/git/tests/test_mapping.py b/breezy/git/tests/test_mapping.py index a7b1cc361b..349e9a479a 100644 --- a/breezy/git/tests/test_mapping.py +++ b/breezy/git/tests/test_mapping.py @@ -142,6 +142,26 @@ def test_explicit_encoding(self): self.assertEqual("iso8859-1", rev.properties[u"git-explicit-encoding"]) self.assertTrue(u"git-implicit-encoding" not in rev.properties) + def test_explicit_encoding_false(self): + c = Commit() + c.tree = b"cc9462f7f8263ef5adfbeff2fb936bb36b504cba" + c.message = b"Some message" + c.committer = b"Committer" + c.commit_time = 4 + c.author_time = 5 + c.commit_timezone = 60 * 5 + c.author_timezone = 60 * 3 + c.author = u"Authér".encode("utf-8") + c.encoding = b"false" + mapping = BzrGitMappingv1() + rev, roundtrip_revid, verifiers = mapping.import_commit( + c, mapping.revision_id_foreign_to_bzr) + self.assertEqual(None, roundtrip_revid) + self.assertEqual({}, verifiers) + self.assertEqual(u"Authér", rev.properties[u'author']) + self.assertEqual("false", rev.properties[u"git-explicit-encoding"]) + self.assertTrue(u"git-implicit-encoding" not in rev.properties) + def test_implicit_encoding_fallback(self): c = Commit() c.tree = b"cc9462f7f8263ef5adfbeff2fb936bb36b504cba" From 11dff8a800b644b1481de1bdc6441ee1fdfebd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 23 Jan 2021 02:45:17 +0000 Subject: [PATCH 02/24] Fix handling of some git fetches. --- breezy/git/interrepo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/breezy/git/interrepo.py b/breezy/git/interrepo.py index e54e2a4035..8b15058929 100644 --- a/breezy/git/interrepo.py +++ b/breezy/git/interrepo.py @@ -655,7 +655,7 @@ def fetch_refs(self, update_refs, lossy, overwrite=False): def determine_wants(heads): old_refs = dict([(k, (v, None)) - for (k, v) in viewitems(heads.as_dict())]) + for (k, v) in viewitems(heads)]) new_refs = update_refs(old_refs) ref_changes.update(new_refs) return [sha1 for (sha1, bzr_revid) in viewvalues(new_refs)] @@ -807,9 +807,9 @@ def fetch_refs(self, update_refs, lossy=False, overwrite=False): def git_update_refs(old_refs): ret = {} self.old_refs = { - k: (v, None) for (k, v) in viewitems(old_refs)} + k: (v, None) for (k, v) in old_refs.items()} new_refs = update_refs(self.old_refs) - for name, (gitid, revid) in viewitems(new_refs): + for name, (gitid, revid) in new_refs.items(): if gitid is None: gitid = self.source_store._lookup_revision_sha1(revid) if not overwrite: From ec4ea6f7f464f9c3bbe42a343ff35792e7dffb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 23 Jan 2021 02:54:05 +0000 Subject: [PATCH 03/24] More fixes. --- breezy/git/branch.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/breezy/git/branch.py b/breezy/git/branch.py index 3b68dd7b8f..de9be379f8 100644 --- a/breezy/git/branch.py +++ b/breezy/git/branch.py @@ -1455,13 +1455,26 @@ def fetch(self, stop_revision=None, fetch_tags=None, lossy=False, for k, v in viewitems(self.source.tags.get_tag_dict()): ret.append((None, v)) ret.append((None, stop_revision)) - try: - revidmap = self.interrepo.fetch_revs(ret, lossy=lossy, limit=limit) - except NoPushSupport: - raise errors.NoRoundtrippingSupport(self.source, self.target) - return _mod_repository.FetchResult(revidmap={ - old_revid: new_revid - for (old_revid, (new_sha, new_revid)) in revidmap.items()}) + if getattr(self.interrepo, 'fetch_revs', None): + try: + revidmap = self.interrepo.fetch_revs(ret, lossy=lossy, limit=limit) + except NoPushSupport: + raise errors.NoRoundtrippingSupport(self.source, self.target) + return _mod_repository.FetchResult(revidmap={ + old_revid: new_revid + for (old_revid, (new_sha, new_revid)) in revidmap.items()}) + else: + def determine_wants(refs): + wants = [] + for git_sha, revid in ret: + if git_sha is None: + git_sha, mapping = self.target.lookup_bzr_revision_id(revid) + wants.append(git_sha) + return wants + + self.interrepo.fetch_objects( + determine_wants, lossy=lossy, limit=limit) + return _mod_repository.FetchResult() def pull(self, overwrite=False, stop_revision=None, local=False, possible_transports=None, run_hooks=True, _stop_revno=None, From 9bac2e021198c2e9c83fee046110e779a98cb075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 2 Feb 2021 20:16:52 +0000 Subject: [PATCH 04/24] Add a create_project method to GitLab. --- breezy/plugins/gitlab/hoster.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 607f2491ee..2df8d4d882 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -412,6 +412,14 @@ def _get_project(self, project_name, _redirect_checked=False): return json.loads(response.data) _unexpected_status(path, response) + def create_project(self, project_name): + fields = {'name': project_name} + response = self._api_request('POST', 'projects', fields=fields) + if response.status not in (200, 201): + _unexpected_status('projects', response) + project = json.loads(response.data) + return project + def _fork_project(self, project_name, timeout=50, interval=5, owner=None): path = 'projects/%s/fork' % urlutils.quote(str(project_name), '') fields = {} From 072b0d19e071711b00d86daab0eba32dcb5b2da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 10 Feb 2021 17:57:32 +0000 Subject: [PATCH 05/24] Migrate to github actions for 3.1 branch. --- .github/workflows/pythonpackage.yml | 49 +++++++++++++++++++++++++++++ .travis.yml | 31 ------------------ 2 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/pythonpackage.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000000..12fca0d2c2 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,49 @@ +name: Python package + +on: [push, pull_request] + +jobs: + build: + + continue-on-error: ${{ matrix.experimental }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + experimental: [false] +# See https://github.com/actions/toolkit/issues/399 +# include: +# - os: ubuntu-latest +# python-version: pypy3 +# experimental: true + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies (apt) + run: | + sudo apt install quilt + if: "matrix.os == 'ubuntu-latest'" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U pip setuptools + pip install -U pip coverage codecov flake8 testtools paramiko fastimport configobj cython testscenarios six docutils $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff pyinotify git+https://github.com/dulwich/dulwich + - name: Build docs + run: | + make docs PYTHON=python + - name: Build extensions + run: | + make extensions PYTHON=python + if: "matrix.python-version != 'pypy3'" + - name: Test suite run + run: | + python -Werror -Wignore::ImportWarning -Wignore::PendingDeprecationWarning -Wignore::DeprecationWarning -Wignore::ResourceWarning -Wignore::UserWarning ./brz selftest + env: + PYTHONHASHSEED: random + BRZ_PLUGIN_PATH: -site:-user diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 59d458423b..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: python -addons: - apt: - update: true -sudo: true -cache: pip - -python: - - 3.5 - - 3.6 - -matrix: - include: - - python: 2.7 - env: SELFTEST_OPTIONS="--coverage" - - python: 3.7 - dist: xenial - - python: 3.8 - dist: xenial - -script: - - make docs extensions PYTHON=python - - BRZ_PLUGIN_PATH=-site:-user python -Werror -Wignore::ImportWarning -Wignore::PendingDeprecationWarning -Wignore::DeprecationWarning -Wignore::ResourceWarning -Wignore::UserWarning ./brz selftest --parallel=fork $SELFTEST_OPTIONS - -install: - - sudo apt install python-all-dev python3-all-dev subunit quilt - - travis_retry pip install -U setuptools - - travis_retry pip install -U pip coverage codecov flake8 testtools paramiko fastimport configobj cython testscenarios six docutils python-subunit $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff git+https://github.com/dulwich/dulwich - -after_success: - - codecov From 674eb8e77a9d978d9abae06501e18e7528fba4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 24 Feb 2021 17:01:40 +0000 Subject: [PATCH 06/24] Document policy for reporting security issues. --- SECURITY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..7df527e202 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| -------- | ------------------ | +| 3.1.x | :white_check_mark: | +| 3.0.x | :x: | + +## Reporting a Vulnerability + +Please report security issues by e-mail to breezy-core@googlegroups.com. From 34dc6058f4e8648b70a534dcd448ae6671286d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 24 Feb 2021 17:14:42 +0000 Subject: [PATCH 07/24] Add a pypi plugin. --- breezy/directory_service.py | 6 +-- breezy/plugins/pypi/__init__.py | 29 ++++++++++++++ breezy/plugins/pypi/directory.py | 66 ++++++++++++++++++++++++++++++++ doc/en/release-notes/brz-3.1.txt | 3 ++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 breezy/plugins/pypi/__init__.py create mode 100644 breezy/plugins/pypi/directory.py diff --git a/breezy/directory_service.py b/breezy/directory_service.py index 63075cc4f6..1063a1b67c 100644 --- a/breezy/directory_service.py +++ b/breezy/directory_service.py @@ -85,11 +85,7 @@ def dereference(self, url, purpose=None): return url service, name = match directory = service() - try: - return directory.look_up(name, url, purpose=purpose) - except TypeError: - # Compatibility for plugins written for Breezy < 3.0.0 - return directory.look_up(name, url) + return directory.look_up(name, url, purpose=purpose) directories = DirectoryServiceRegistry() diff --git a/breezy/plugins/pypi/__init__.py b/breezy/plugins/pypi/__init__.py new file mode 100644 index 0000000000..85c187860c --- /dev/null +++ b/breezy/plugins/pypi/__init__.py @@ -0,0 +1,29 @@ +# Copyright (C) 2021 Breezy Developers +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Support for looking up URLs from pypi. +""" + +from __future__ import absolute_import + +from ... import ( + version_info, # noqa: F401 + ) +from ...directory_service import directories + +directories.register_lazy('pypi:', __name__ + '.directory', + 'PypiDirectory', + 'Pypi-based directory service',) diff --git a/breezy/plugins/pypi/directory.py b/breezy/plugins/pypi/directory.py new file mode 100644 index 0000000000..eeb7708a7a --- /dev/null +++ b/breezy/plugins/pypi/directory.py @@ -0,0 +1,66 @@ +# Copyright (C) 2021 Breezy Developers +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Directory lookup that uses pypi.""" + +from __future__ import absolute_import + +from breezy.errors import BzrError +from breezy.trace import note +from breezy.urlutils import InvalidURL + +import json + +try: + from urllib.request import urlopen + from urllib.error import HTTPError +except ImportError: # python < 3 + from urllib import urlopen, HTTPError + + +class PypiProjectWithoutRepositoryURL(InvalidURL): + + _fmt = "No repository URL set for pypi project %(name)s" + + def __init__(self, name, url=None): + BzrError.__init__(self, name=name, url=url) + + +class NoSuchPypiProject(InvalidURL): + + _fmt = "No pypi project with name %(name)s" + + def __init__(self, name, url=None): + BzrError.__init__(self, name=name, url=url) + + +class PypiDirectory(object): + + def look_up(self, name, url, purpose=None): + """See DirectoryService.look_up""" + try: + with urlopen('https://pypi.org/pypi/%s/json' % name) as f: + data = json.load(f) + except HTTPError as e: + if e.status == 404: + raise NoSuchPypiProject(name, url=url) + raise + for key, value in data['info']['project_urls'].items(): + if key == 'Repository': + note('Found repository URL %s for pypi project %s', + value, name) + return value + raise PypiProjectWithoutRepositoryURL(name, url=url) diff --git a/doc/en/release-notes/brz-3.1.txt b/doc/en/release-notes/brz-3.1.txt index cb26ae94e1..933c02c046 100644 --- a/doc/en/release-notes/brz-3.1.txt +++ b/doc/en/release-notes/brz-3.1.txt @@ -54,6 +54,9 @@ Improvements but it prevents the DWIM revision specifier from treating "svn:" as a URL. (Jelmer Vernooij) + * New `pypi` directory that can be used to access remote repositories + declared in pypi. (Jelmer Vernooij) + Bug Fixes ********* From f897059ec09eb65c549d59d93a91675bff213252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 24 Feb 2021 17:25:54 +0000 Subject: [PATCH 08/24] Add support for recognizing GitHub URLs. --- breezy/plugins/pypi/directory.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/breezy/plugins/pypi/directory.py b/breezy/plugins/pypi/directory.py index eeb7708a7a..8d351c2858 100644 --- a/breezy/plugins/pypi/directory.py +++ b/breezy/plugins/pypi/directory.py @@ -26,9 +26,11 @@ try: from urllib.request import urlopen + from urllib.parse import urlparse from urllib.error import HTTPError except ImportError: # python < 3 from urllib import urlopen, HTTPError + from urlparse import urlparse class PypiProjectWithoutRepositoryURL(InvalidURL): @@ -47,6 +49,18 @@ def __init__(self, name, url=None): BzrError.__init__(self, name=name, url=url) +def find_repo_url(data): + for key, value in data['info']['project_urls'].items(): + if key == 'Repository': + note('Found repository URL %s for pypi project %s', + value, name) + return value + parsed_url = urlparse(value) + if (parsed_url.hostname == 'github.com' and + parsed_url.path.strip('/').count('/') == 1): + return value + + class PypiDirectory(object): def look_up(self, name, url, purpose=None): @@ -58,9 +72,7 @@ def look_up(self, name, url, purpose=None): if e.status == 404: raise NoSuchPypiProject(name, url=url) raise - for key, value in data['info']['project_urls'].items(): - if key == 'Repository': - note('Found repository URL %s for pypi project %s', - value, name) - return value - raise PypiProjectWithoutRepositoryURL(name, url=url) + url = find_repo_url(data) + if url is None: + raise PypiProjectWithoutRepositoryURL(name, url=url) + return url From b9608b216494f4c3aacae01b532eb256f9f37f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 1 Mar 2021 18:48:02 +0000 Subject: [PATCH 09/24] Don't drop backwards compatibility. --- breezy/directory_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/breezy/directory_service.py b/breezy/directory_service.py index 1063a1b67c..63075cc4f6 100644 --- a/breezy/directory_service.py +++ b/breezy/directory_service.py @@ -85,7 +85,11 @@ def dereference(self, url, purpose=None): return url service, name = match directory = service() - return directory.look_up(name, url, purpose=purpose) + try: + return directory.look_up(name, url, purpose=purpose) + except TypeError: + # Compatibility for plugins written for Breezy < 3.0.0 + return directory.look_up(name, url) directories = DirectoryServiceRegistry() From e281b0795cbefec7c301348530a8e2b3b0046deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 7 Mar 2021 16:12:16 +0000 Subject: [PATCH 10/24] Make fork-project public. --- breezy/plugins/gitlab/hoster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 2df8d4d882..5d1fa241ea 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -420,7 +420,7 @@ def create_project(self, project_name): project = json.loads(response.data) return project - def _fork_project(self, project_name, timeout=50, interval=5, owner=None): + def fork_project(self, project_name, timeout=50, interval=5, owner=None): path = 'projects/%s/fork' % urlutils.quote(str(project_name), '') fields = {} if owner is not None: @@ -568,7 +568,7 @@ def publish_derived(self, local_branch, base_branch, name, project=None, try: target_project = self._get_project('%s/%s' % (owner, project)) except NoSuchProject: - target_project = self._fork_project( + target_project = self.fork_project( base_project['path_with_namespace'], owner=owner) remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo']) remote_dir = controldir.ControlDir.open(remote_repo_url) From c45e318fd72cd086ff466ab3efb36b08c46d65d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 9 Mar 2021 21:03:15 +0000 Subject: [PATCH 11/24] Also refresh old_sha1 when overwriting. --- breezy/git/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/git/remote.py b/breezy/git/remote.py index 063ab0eb6d..71c6b4281e 100644 --- a/breezy/git/remote.py +++ b/breezy/git/remote.py @@ -640,8 +640,8 @@ def get_changed_refs(remote_refs): except errors.NoSuchRevision: raise errors.NoRoundtrippingSupport( source, self.open_branch(name=name, nascent_ok=True)) + old_sha = remote_refs.get(actual_refname) if not overwrite: - old_sha = remote_refs.get(actual_refname) if remote_divergence(old_sha, new_sha, source_store): raise DivergedBranches( source, self.open_branch(name, nascent_ok=True)) From 528fb1b9602e8ff87d187a9722d7c938d4ebec7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 10 Mar 2021 00:31:43 +0000 Subject: [PATCH 12/24] Add preferred_schemes to Hoster.get_derived_branch. --- breezy/plugins/github/hoster.py | 20 +++++++++++++++++--- breezy/plugins/gitlab/hoster.py | 17 ++++++++++++++--- breezy/plugins/launchpad/hoster.py | 3 ++- breezy/propose.py | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/breezy/plugins/github/hoster.py b/breezy/plugins/github/hoster.py index 45126a4721..567c68a490 100644 --- a/breezy/plugins/github/hoster.py +++ b/breezy/plugins/github/hoster.py @@ -461,7 +461,7 @@ def get_push_url(self, branch): repo = self._get_repo(owner, project) return github_url_to_bzr_url(repo['ssh_url'], branch_name) - def get_derived_branch(self, base_branch, name, project=None, owner=None): + def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None): base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch) base_repo = self._get_repo(base_owner, base_project) if owner is None: @@ -470,10 +470,24 @@ def get_derived_branch(self, base_branch, name, project=None, owner=None): project = base_repo['name'] try: remote_repo = self._get_repo(owner, project) - full_url = github_url_to_bzr_url(remote_repo['ssh_url'], name) - return _mod_branch.Branch.open(full_url) except NoSuchProject: raise errors.NotBranchError('%s/%s/%s' % (WEB_GITHUB_URL, owner, project)) + if preferred_schemes is None: + preferred_schemes = ['git+ssh'] + for scheme in preferred_schemes: + if scheme == 'git+ssh': + github_url = remote_repo['ssh_url'] + break + if scheme == 'https': + github_url = remote_repo['clone_url'] + break + if scheme == 'git': + github_url = remote_repo['git_url'] + break + else: + raise AssertionError + full_url = github_url_to_bzr_url(github_url, name) + return _mod_branch.Branch.open(full_url) def get_proposer(self, source_branch, target_branch): return GitHubMergeProposalBuilder(self, source_branch, target_branch) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 5d1fa241ea..bf43089df6 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -586,7 +586,7 @@ def publish_derived(self, local_branch, base_branch, name, project=None, target_project['http_url_to_repo'], name) return push_result.target_branch, public_url - def get_derived_branch(self, base_branch, name, project=None, owner=None): + def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None): (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch) if owner is None: owner = self.get_current_user() @@ -596,8 +596,19 @@ def get_derived_branch(self, base_branch, name, project=None, owner=None): target_project = self._get_project('%s/%s' % (owner, project)) except NoSuchProject: raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project)) - return _mod_branch.Branch.open(gitlab_url_to_bzr_url( - target_project['ssh_url_to_repo'], name)) + if preferred_schemes is None: + preferred_schemes = ['git+ssh'] + for scheme in preferred_schemes: + if scheme == 'git+ssh': + gitlab_url = target_project['ssh_url_to_repo'] + break + elif scheme == 'https': + gitlab_url = target_project['http_url_to_repo'] + break + else: + raise AssertionError + return _mod_branch.Branch.open( + gitlab_url_to_bzr_url(gitlab_url, name)) def get_proposer(self, source_branch, target_branch): return GitlabMergeProposalBuilder(self, source_branch, target_branch) diff --git a/breezy/plugins/launchpad/hoster.py b/breezy/plugins/launchpad/hoster.py index 9bbba260fb..b0f20964ac 100644 --- a/breezy/plugins/launchpad/hoster.py +++ b/breezy/plugins/launchpad/hoster.py @@ -410,7 +410,8 @@ def publish_derived(self, local_branch, base_branch, name, project=None, else: raise AssertionError('not a valid Launchpad URL') - def get_derived_branch(self, base_branch, name, project=None, owner=None): + def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None): + # TODO(jelmer): honor preferred_schemes if owner is None: owner = self.launchpad.me.name (base_vcs, base_user, base_password, base_path, diff --git a/breezy/propose.py b/breezy/propose.py index ca11b909cc..c654d23f9c 100644 --- a/breezy/propose.py +++ b/breezy/propose.py @@ -288,7 +288,7 @@ def publish_derived(self, new_branch, base_branch, name, project=None, """ raise NotImplementedError(self.publish_derived) - def get_derived_branch(self, base_branch, name, project=None, owner=None): + def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None): """Get a derived branch ('a fork'). """ raise NotImplementedError(self.get_derived_branch) From 189d63ed7151f12100f06c540fe4ec57a4f2091b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 14 Mar 2021 20:18:53 +0000 Subject: [PATCH 13/24] Return list of conflicts. --- breezy/builtins.py | 4 ++-- breezy/bzr/workingtree.py | 4 ++-- breezy/git/workingtree.py | 4 ++-- breezy/merge.py | 2 +- breezy/plugins/quilt/tests/test_merge.py | 4 ++-- breezy/tests/per_workingtree/test_commit.py | 6 +++--- .../per_workingtree/test_merge_from_branch.py | 18 +++++++++--------- breezy/tests/per_workingtree/test_unversion.py | 6 +++--- breezy/tests/test_merge.py | 10 +++++----- breezy/tests/test_merge_core.py | 16 +++++++++------- doc/en/release-notes/brz-3.1.txt | 4 ++++ 11 files changed, 42 insertions(+), 36 deletions(-) diff --git a/breezy/builtins.py b/breezy/builtins.py index b81236f138..a74366f18f 100644 --- a/breezy/builtins.py +++ b/breezy/builtins.py @@ -4593,7 +4593,7 @@ def _do_preview(self, merger): def _do_merge(self, merger, change_reporter, allow_pending, verified): merger.change_reporter = change_reporter - conflict_count = merger.do_merge() + conflict_count = len(merger.do_merge()) if allow_pending: merger.set_pending() if verified == 'failed': @@ -4851,7 +4851,7 @@ def run(self, file_list=None, merge_type=None, show_base=False, conflicts = merger.do_merge() finally: tree.set_parent_ids(parents) - if conflicts > 0: + if len(conflicts) > 0: return 1 else: return 0 diff --git a/breezy/bzr/workingtree.py b/breezy/bzr/workingtree.py index 86034985d3..f08a54fadf 100644 --- a/breezy/bzr/workingtree.py +++ b/breezy/bzr/workingtree.py @@ -1958,7 +1958,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, show_base=show_base) if nb_conflicts: self.add_parent_tree((old_tip, other_tree)) - return nb_conflicts + return len(nb_conflicts) if last_rev != _mod_revision.ensure_null(revision): # the working tree is up to date with the branch @@ -2000,7 +2000,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, (old_tip, self.branch.repository.revision_tree(old_tip))) self.set_parent_trees(parent_trees) last_rev = parent_trees[0][0] - return nb_conflicts + return len(nb_conflicts) class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat): diff --git a/breezy/git/workingtree.py b/breezy/git/workingtree.py index b10cce9c6b..fa88409a0a 100644 --- a/breezy/git/workingtree.py +++ b/breezy/git/workingtree.py @@ -1542,7 +1542,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, show_base=show_base) if nb_conflicts: self.add_parent_tree((old_tip, other_tree)) - return nb_conflicts + return len(nb_conflicts) if last_rev != _mod_revision.ensure_null(revision): to_tree = self.branch.repository.revision_tree(revision) @@ -1575,7 +1575,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, (old_tip, self.branch.repository.revision_tree(old_tip))) self.set_parent_trees(parent_trees) last_rev = parent_trees[0][0] - return nb_conflicts + return len(nb_conflicts) class GitWorkingTreeFormat(workingtree.WorkingTreeFormat): diff --git a/breezy/merge.py b/breezy/merge.py index cd11fdb04c..4904cb308f 100644 --- a/breezy/merge.py +++ b/breezy/merge.py @@ -670,7 +670,7 @@ def do_merge(self): trace.note(gettext("%d conflicts encountered.") % len(merge.cooked_conflicts)) - return len(merge.cooked_conflicts) + return merge.cooked_conflicts class _InventoryNoneEntry(object): diff --git a/breezy/plugins/quilt/tests/test_merge.py b/breezy/plugins/quilt/tests/test_merge.py index 889fa5fbce..49c715c330 100644 --- a/breezy/plugins/quilt/tests/test_merge.py +++ b/breezy/plugins/quilt/tests/test_merge.py @@ -151,7 +151,7 @@ def test_diverged_patches(self): """, "a/debian/patches/patch1") # "a" should be unapplied again self.assertPathDoesNotExist("a/a") - self.assertEquals(1, conflicts) + self.assertEquals(1, len(conflicts)) def test_auto_apply_patches_after_checkout(self): self.enable_hooks() @@ -288,7 +288,7 @@ def test_disabled_hook(self): c >>>>>>> MERGE-SOURCE """, "a/a") - self.assertEquals(2, conflicts) + self.assertEquals(2, len(conflicts)) diff --git a/breezy/tests/per_workingtree/test_commit.py b/breezy/tests/per_workingtree/test_commit.py index 78b871b6e5..b97437140d 100644 --- a/breezy/tests/per_workingtree/test_commit.py +++ b/breezy/tests/per_workingtree/test_commit.py @@ -101,11 +101,11 @@ def test_no_autodelete_alternate_renamed(self): # Merging from A should introduce conflicts because 'n' was modified # (in A) and removed (in B), so 'a' needs to be restored. - num_conflicts = tree_b.merge_from_branch(tree_a.branch) + conflicts = tree_b.merge_from_branch(tree_a.branch) if tree_b.has_versioned_directories(): - self.assertEqual(3, num_conflicts) + self.assertEqual(3, len(conflicts)) else: - self.assertEqual(2, num_conflicts) + self.assertEqual(2, len(num_conflicts)) self.assertThat( tree_b, HasPathRelations( diff --git a/breezy/tests/per_workingtree/test_merge_from_branch.py b/breezy/tests/per_workingtree/test_merge_from_branch.py index 335e3f6168..fbf870f01f 100644 --- a/breezy/tests/per_workingtree/test_merge_from_branch.py +++ b/breezy/tests/per_workingtree/test_merge_from_branch.py @@ -224,9 +224,9 @@ def test_file3_in_root_conflicted(self): outer.commit('delete file3') nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[2]) if outer.supports_rename_tracking(): - self.assertEqual(4, nb_conflicts) + self.assertEqual(4, len(nb_conflicts)) else: - self.assertEqual(1, nb_conflicts) + self.assertEqual(1, len(nb_conflicts)) self.assertTreeLayout(['dir-outer', 'dir-outer/dir', 'dir-outer/dir/file1', @@ -245,9 +245,9 @@ def test_file4_added_in_root(self): # file4 could not be added to its original root, so it gets added to # the new root with a conflict. if outer.supports_rename_tracking(): - self.assertEqual(1, nb_conflicts) + self.assertEqual(1, len(nb_conflicts)) else: - self.assertEqual(0, nb_conflicts) + self.assertEqual(0, len(nb_conflicts)) self.assertTreeLayout(['dir-outer', 'dir-outer/dir', 'dir-outer/dir/file1', @@ -261,9 +261,9 @@ def test_file4_added_then_renamed(self): # 1 conflict, because file4 can't be put into the old root nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[3]) if outer.supports_rename_tracking(): - self.assertEqual(1, nb_conflicts) + self.assertEqual(1, len(nb_conflicts)) else: - self.assertEqual(0, nb_conflicts) + self.assertEqual(0, len(nb_conflicts)) try: outer.set_conflicts([]) except errors.UnsupportedOperation: @@ -275,7 +275,7 @@ def test_file4_added_then_renamed(self): # And now file4 gets renamed into an existing dir nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[4]) if outer.supports_rename_tracking(): - self.assertEqual(1, nb_conflicts) + self.assertEqual(1, len(nb_conflicts)) self.assertTreeLayout(['dir-outer', 'dir-outer/dir', 'dir-outer/dir/file1', @@ -285,9 +285,9 @@ def test_file4_added_then_renamed(self): outer) else: if outer.has_versioned_directories(): - self.assertEqual(2, nb_conflicts) + self.assertEqual(2, len(nb_conflicts)) else: - self.assertEqual(1, nb_conflicts) + self.assertEqual(1, len(nb_conflicts)) self.assertTreeLayout(['dir', 'dir-outer', 'dir-outer/dir', diff --git a/breezy/tests/per_workingtree/test_unversion.py b/breezy/tests/per_workingtree/test_unversion.py index d07bed9f7a..9866cba8ab 100644 --- a/breezy/tests/per_workingtree/test_unversion.py +++ b/breezy/tests/per_workingtree/test_unversion.py @@ -175,11 +175,11 @@ def test_unversion_after_conflicted_merge(self): # Merging from A should introduce conflicts because 'n' was modified # and removed, so 'a' needs to be restored. We also have a conflict # because 'a' is still an existing directory - num_conflicts = tree_b.merge_from_branch(tree_a.branch) + conflicts = tree_b.merge_from_branch(tree_a.branch) if tree_b.has_versioned_directories(): - self.assertEqual(4, num_conflicts) + self.assertEqual(4, len(num_conflicts)) else: - self.assertEqual(1, num_conflicts) + self.assertEqual(1, len(num_conflicts)) self.assertThat( tree_b, diff --git a/breezy/tests/test_merge.py b/breezy/tests/test_merge.py index 49e80f179a..0cc15b2e25 100644 --- a/breezy/tests/test_merge.py +++ b/breezy/tests/test_merge.py @@ -2287,7 +2287,7 @@ def test_executable_changes(self): wt.revert() self.assertFalse(wt.is_executable('foo')) conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual(0, len(conflicts)) self.assertTrue(wt.is_executable('foo')) def test_create_symlink(self): @@ -2323,7 +2323,7 @@ def test_create_symlink(self): wt.revert() self.assertFalse(wt.is_versioned('foo')) conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual(0, len(conflicts)) self.assertEqual(b'foo-id', wt.path2id('foo')) self.assertEqual('bar', wt.get_symlink_target('foo')) @@ -2403,7 +2403,7 @@ def test_modified_symlink(self): wt.merge_from_branch(wt.branch, b'C-id') wt.commit('D merges B & C', rev_id=b'D-id') conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual(0, len(conflicts)) self.assertEqual('bing', wt.get_symlink_target('foo')) def test_renamed_symlink(self): @@ -2460,7 +2460,7 @@ def test_renamed_symlink(self): False), ], entries) conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual(0, len(conflicts)) self.assertEqual('blah', wt.id2path(b'foo-id')) def test_symlink_no_content_change(self): @@ -2510,7 +2510,7 @@ def test_symlink_no_content_change(self): self.assertEqual([], list(merge_obj._entries_lca())) # Now do a real merge, just to test the rest of the stack conflicts = wt.merge_from_branch(wt.branch, to_revision=b'E-id') - self.assertEqual(0, conflicts) + self.assertEqual(0, len(conflicts)) self.assertEqual('bing', wt.get_symlink_target('foo')) def test_symlink_this_changed_kind(self): diff --git a/breezy/tests/test_merge_core.py b/breezy/tests/test_merge_core.py index e9d4fa02b7..3045f60f7d 100644 --- a/breezy/tests/test_merge_core.py +++ b/breezy/tests/test_merge_core.py @@ -503,13 +503,13 @@ def test_conflicts(self): self.build_tree_contents([('b/file', b'this contents contents\n')]) wtb = d_b.open_workingtree() wtb.commit('this revision', allow_pointless=False) - self.assertEqual(1, wtb.merge_from_branch(wta.branch)) + self.assertEqual(1, len(wtb.merge_from_branch(wta.branch))) self.assertPathExists('b/file.THIS') self.assertPathExists('b/file.BASE') self.assertPathExists('b/file.OTHER') wtb.revert() - self.assertEqual(1, wtb.merge_from_branch(wta.branch, - merge_type=WeaveMerger)) + self.assertEqual(1, len(wtb.merge_from_branch(wta.branch, + merge_type=WeaveMerger))) self.assertPathExists('b/file') self.assertPathExists('b/file.THIS') self.assertPathExists('b/file.BASE') @@ -544,9 +544,9 @@ def test_weave_conflicts_not_in_base(self): revision_id=b'E-id') builder.finish_series() tree = builder.get_branch().create_checkout('tree', lightweight=True) - self.assertEqual(1, tree.merge_from_branch(tree.branch, + self.assertEqual(1, len(tree.merge_from_branch(tree.branch, to_revision=b'D-id', - merge_type=WeaveMerger)) + merge_type=WeaveMerger))) self.assertPathExists('tree/foo.THIS') self.assertPathExists('tree/foo.OTHER') self.expectFailure('fail to create .BASE in some criss-cross merges', @@ -640,8 +640,10 @@ def test_merge_swapping_renames(self): b_wt.rename_one('deux', 'un') b_wt.rename_one('tmp', 'deux') b_wt.commit('r1', rev_id=b'r1') - self.assertEqual(0, a_wt.merge_from_branch(b_wt.branch, - b_wt.branch.last_revision(), b_wt.branch.get_rev_id(1))) + self.assertEqual( + 0, len(a_wt.merge_from_branch( + b_wt.branch, b_wt.branch.last_revision(), + b_wt.branch.get_rev_id(1)))) self.assertPathExists('a/un') self.assertTrue('a/deux') self.assertFalse(os.path.exists('a/tmp')) diff --git a/doc/en/release-notes/brz-3.1.txt b/doc/en/release-notes/brz-3.1.txt index 933c02c046..70a6b55aee 100644 --- a/doc/en/release-notes/brz-3.1.txt +++ b/doc/en/release-notes/brz-3.1.txt @@ -99,6 +99,10 @@ API Changes * File ids are no longer returned in ``Tree.walkdirs``. (Jelmer Vernooij) + * ``WorkingTree.merge_from_branch``, ``Merge.do_merge`` and + ``merge_inner`` now return a list of conflicts rather than number of + conflicts. (Jelmer Vernooij) + Internals ********* From 233cf4742f40000d82d5cd2b09e02d9eb7123e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 14 Mar 2021 21:18:43 +0000 Subject: [PATCH 14/24] Fix tests. --- breezy/bzr/workingtree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/bzr/workingtree.py b/breezy/bzr/workingtree.py index f08a54fadf..82674fb6ba 100644 --- a/breezy/bzr/workingtree.py +++ b/breezy/bzr/workingtree.py @@ -1937,7 +1937,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, # local work is unreferenced and will appear to have been lost. # with self.lock_tree_write(): - nb_conflicts = 0 + nb_conflicts = [] try: last_rev = self.get_parent_ids()[0] except IndexError: From 16ef3f9a9880bb9c71eae00767bcd6573923f680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 14 Mar 2021 22:04:35 +0000 Subject: [PATCH 15/24] Fix tests --- breezy/git/workingtree.py | 2 +- breezy/tests/per_workingtree/test_commit.py | 2 +- .../tests/per_workingtree/test_unversion.py | 4 +-- breezy/tests/test_merge.py | 26 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/breezy/git/workingtree.py b/breezy/git/workingtree.py index fa88409a0a..bb4fa2d15c 100644 --- a/breezy/git/workingtree.py +++ b/breezy/git/workingtree.py @@ -1521,7 +1521,7 @@ def _update_tree(self, old_tip=None, change_reporter=None, revision=None, # with self.lock_tree_write(): from .. import merge - nb_conflicts = 0 + nb_conflicts = [] try: last_rev = self.get_parent_ids()[0] except IndexError: diff --git a/breezy/tests/per_workingtree/test_commit.py b/breezy/tests/per_workingtree/test_commit.py index b97437140d..b5c015a860 100644 --- a/breezy/tests/per_workingtree/test_commit.py +++ b/breezy/tests/per_workingtree/test_commit.py @@ -105,7 +105,7 @@ def test_no_autodelete_alternate_renamed(self): if tree_b.has_versioned_directories(): self.assertEqual(3, len(conflicts)) else: - self.assertEqual(2, len(num_conflicts)) + self.assertEqual(2, len(conflicts)) self.assertThat( tree_b, HasPathRelations( diff --git a/breezy/tests/per_workingtree/test_unversion.py b/breezy/tests/per_workingtree/test_unversion.py index 9866cba8ab..1809561d1d 100644 --- a/breezy/tests/per_workingtree/test_unversion.py +++ b/breezy/tests/per_workingtree/test_unversion.py @@ -177,9 +177,9 @@ def test_unversion_after_conflicted_merge(self): # because 'a' is still an existing directory conflicts = tree_b.merge_from_branch(tree_a.branch) if tree_b.has_versioned_directories(): - self.assertEqual(4, len(num_conflicts)) + self.assertEqual(4, len(conflicts)) else: - self.assertEqual(1, len(num_conflicts)) + self.assertEqual(1, len(conflicts)) self.assertThat( tree_b, diff --git a/breezy/tests/test_merge.py b/breezy/tests/test_merge.py index 0cc15b2e25..0cfad006ac 100644 --- a/breezy/tests/test_merge.py +++ b/breezy/tests/test_merge.py @@ -435,8 +435,8 @@ def test_merge_reverse_revision_range(self): first_rev) merger.merge_type = _mod_merge.Merge3Merger merger.interesting_files = 'a' - conflict_count = merger.do_merge() - self.assertEqual(0, conflict_count) + conflicts = merger.do_merge() + self.assertEqual([], conflicts) self.assertPathDoesNotExist("a") tree.revert() @@ -516,8 +516,8 @@ def test_merge_require_tree_root(self): _mod_revision.NULL_REVISION, first_rev) merger.merge_type = _mod_merge.Merge3Merger - conflict_count = merger.do_merge() - self.assertEqual(0, conflict_count) + conflicts = merger.do_merge() + self.assertEqual([], conflicts) self.assertEqual({''}, set(tree.all_versioned_paths())) tree.set_parent_ids([]) @@ -2191,7 +2191,7 @@ def test_simple_lca(self): [('modify', ('a', b'a\nb\nc\nd\ne\nf\n'))], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'E-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) # The merge should have simply update the contents of 'a' self.assertEqual(b'a\nb\nc\nd\ne\nf\n', wt.get_file_text('a')) @@ -2221,7 +2221,7 @@ def test_conflict_without_lca(self): [('rename', ('bar', 'baz'))], revision_id=b'F-id') builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) # The merge should simply recognize that the final rename takes # precedence self.assertEqual('baz', wt.id2path(b'foo-id')) @@ -2252,7 +2252,7 @@ def test_other_deletes_lca_renames(self): [('unversion', 'bar')], revision_id=b'F-id') builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) self.assertRaises(errors.NoSuchId, wt.id2path, b'foo-id') def test_executable_changes(self): @@ -2353,7 +2353,7 @@ def test_both_sides_revert(self): builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'E-id') - self.assertEqual(1, conflicts) + self.assertEqual(1, len(conflicts)) self.assertEqualDiff(b'<<<<<<< TREE\n' b'B content\n' b'=======\n' @@ -2653,7 +2653,7 @@ def test_other_reverted_path_to_base(self): [('rename', ('bar', 'foo'))], revision_id=b'F-id') # Rename back to BASE builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) self.assertEqual('foo', wt.id2path(b'foo-id')) def test_other_reverted_content_to_base(self): @@ -2674,7 +2674,7 @@ def test_other_reverted_content_to_base(self): revision_id=b'F-id') # Revert back to BASE builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) # TODO: We need to use the per-file graph to properly select a BASE # before this will work. Or at least use the LCA trees to find # the appropriate content base. (which is B, not A). @@ -2698,7 +2698,7 @@ def test_other_modified_content(self): revision_id=b'F-id') # Override B content builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id') wt, conflicts = self.do_merge(builder, b'F-id') - self.assertEqual(0, conflicts) + self.assertEqual([], conflicts) self.assertEqual(b'F content\n', wt.get_file_text('foo')) def test_all_wt(self): @@ -3303,7 +3303,7 @@ def test_name_conflict(self): dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt']) self.setup_simple_branch('src', ['README']) conflicts = self.do_merge_into('src', 'dest/dir') - self.assertEqual(1, conflicts) + self.assertEqual(1, len(conflicts)) dest_wt.lock_read() self.addCleanup(dest_wt.unlock) # The r1-lib1 revision should be merged into this one @@ -3331,7 +3331,7 @@ def test_file_id_conflict(self): # This is an edge case that shouldn't happen to users very often. So # we don't care really about the exact presentation of the conflict, # just that there is one. - self.assertEqual(1, conflicts) + self.assertEqual(1, len(conflicts)) def test_only_subdir(self): """When the location points to just part of a tree, merge just that From 326ba0af3a3cbd44ae5c749639b0438c50b50cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 15 Mar 2021 18:26:02 +0000 Subject: [PATCH 16/24] Handle 403 when creating a project. --- breezy/plugins/gitlab/hoster.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index bf43089df6..b6645d9fce 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -415,6 +415,8 @@ def _get_project(self, project_name, _redirect_checked=False): def create_project(self, project_name): fields = {'name': project_name} response = self._api_request('POST', 'projects', fields=fields) + if response.status == 403: + raise errors.PermissionDenied(response.text) if response.status not in (200, 201): _unexpected_status('projects', response) project = json.loads(response.data) From b4a3d3baf0a8a0e937e15702fc2df6f662fec2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 16 Mar 2021 15:18:26 +0000 Subject: [PATCH 17/24] Fix defaulting to current user in GitLab.iter_my_forks. --- breezy/plugins/gitlab/hoster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index b6645d9fce..4ff9476259 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -683,7 +683,7 @@ def iter_my_proposals(self, status='open', author=None): yield GitLabMergeProposal(self, mp) def iter_my_forks(self, owner=None): - if owner is not None: + if owner is None: owner = self.get_current_user() for project in self._list_projects(owner=owner): base_project = project.get('forked_from_project') From 6d69cdadfd8f168b5e1d34ba379067277fc21f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 21 Mar 2021 14:13:51 +0000 Subject: [PATCH 18/24] Handle 409s better for gitlab. --- breezy/plugins/gitlab/hoster.py | 46 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 4ff9476259..7bdedfca1d 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -84,7 +84,8 @@ class GitLabUnprocessable(errors.BzrError): _fmt = "GitLab can not process request: %(error)s." def __init__(self, error): - errors.BzrError.__init__(self, error=error) + errors.BzrError.__init__(self) + self.error = error class DifferentGitLabInstances(errors.BzrError): @@ -115,7 +116,8 @@ class GitLabConflict(errors.BzrError): _fmt = "Conflict during operation: %(reason)s" def __init__(self, reason): - errors.BzrError(self, reason=reason) + errors.BzrError(self) + self.reason = reason class ForkingDisabled(errors.BzrError): @@ -231,7 +233,13 @@ def __init__(self, gl, mr): self._mr = mr def _update(self, **kwargs): - self.gl._update_merge_request(self._mr['project_id'], self._mr['iid'], kwargs) + try: + self.gl._update_merge_request( + self._mr['project_id'], self._mr['iid'], kwargs) + except GitLabConflict as e: + self.gl._handle_merge_request_conflict( + e.reason, self.get_source_branch_url(), + self._mr['target_project_id']) def __repr__(self): return "<%s at %r>" % (type(self).__name__, self._mr['web_url']) @@ -449,6 +457,18 @@ def fork_project(self, project_name, timeout=50, interval=5, owner=None): project = self._get_project(project['path_with_namespace']) return project + def _handle_merge_request_conflict(self, reason, source_url, target_project): + m = re.fullmatch( + r'Another open merge request already exists for ' + r'this source branch: \!([0-9]+)', + reason['message'][0]) + if m: + merge_id = int(m.group(1)) + mr = self._get_merge_request(target_project, merge_id) + raise MergeProposalExists( + source_url, GitLabMergeProposal(self, mr)) + raise MergeRequestConflict(reason) + def get_current_user(self): return self._current_user['username'] @@ -508,6 +528,8 @@ def _update_merge_request(self, project_id, iid, mr): response = self._api_request('PUT', path, fields=mr) if response.status == 200: return json.loads(response.data) + if response.status == 409: + raise GitLabConflict(json.loads(response.data).get('message')) if response.status == 403: raise errors.PermissionDenied(response.text) _unexpected_status(path, response) @@ -542,7 +564,7 @@ def _create_mergerequest( if response.status == 403: raise errors.PermissionDenied(response.text) if response.status == 409: - raise MergeRequestConflict(json.loads(response.data)) + raise GitLabConflict(json.loads(response.data).get('message')) if response.status == 422: data = json.loads(response.data) raise GitLabUnprocessable(data['error']) @@ -780,18 +802,10 @@ def create_proposal(self, description, reviewers=None, labels=None, kwargs['assignee_ids'].append(user['id']) try: merge_request = self.gl._create_mergerequest(**kwargs) - except MergeRequestConflict as e: - m = re.fullmatch( - r'Another open merge request already exists for ' - r'this source branch: \!([0-9]+)', - e.reason['message'][0]) - if m: - merge_id = int(m.group(1)) - mr = self.gl._get_merge_request( - target_project['path_with_namespace'], merge_id) - raise MergeProposalExists( - self.source_branch.user_url, GitLabMergeProposal(self.gl, mr)) - raise Exception('conflict: %r' % e.reason) + except GitLabConflict as e: + self.gl._handle_merge_request_conflict( + e.reason, self.source_branch.user_url, + target_project['path_with_namespace']) except GitLabUnprocessable as e: if e.error == [ "Source project is not a fork of the target project"]: From edd0282d420161c6b8f14c6eb04823c25e797086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 21 Mar 2021 16:56:29 +0000 Subject: [PATCH 19/24] Support 'checking' merge_status. --- breezy/plugins/gitlab/hoster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 7bdedfca1d..df3b88b84a 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -313,7 +313,7 @@ def can_be_merged(self): elif self._mr['merge_status'] == 'can_be_merged': return True elif self._mr['merge_status'] in ( - 'unchecked', 'cannot_be_merged_recheck'): + 'unchecked', 'cannot_be_merged_recheck', 'checking'): # See https://gitlab.com/gitlab-org/gitlab/-/commit/7517105303c for # an explanation of the distinction between unchecked and # cannot_be_merged_recheck From 54c01438ef7aae08866d2399a484f2d1caab2dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 21 Mar 2021 17:21:18 +0000 Subject: [PATCH 20/24] Fix handling of gitlab conflicts. --- breezy/plugins/gitlab/hoster.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 7bdedfca1d..5bfdd8d252 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -113,11 +113,11 @@ def __init__(self, error): class GitLabConflict(errors.BzrError): - _fmt = "Conflict during operation: %(reason)s" + _fmt = "Conflict during operation: %(message)s" - def __init__(self, reason): + def __init__(self, message): errors.BzrError(self) - self.reason = reason + self.message = message class ForkingDisabled(errors.BzrError): @@ -238,7 +238,7 @@ def _update(self, **kwargs): self._mr['project_id'], self._mr['iid'], kwargs) except GitLabConflict as e: self.gl._handle_merge_request_conflict( - e.reason, self.get_source_branch_url(), + e.message, self.get_source_branch_url(), self._mr['target_project_id']) def __repr__(self): @@ -457,11 +457,11 @@ def fork_project(self, project_name, timeout=50, interval=5, owner=None): project = self._get_project(project['path_with_namespace']) return project - def _handle_merge_request_conflict(self, reason, source_url, target_project): + def _handle_merge_request_conflict(self, message, source_url, target_project): m = re.fullmatch( r'Another open merge request already exists for ' r'this source branch: \!([0-9]+)', - reason['message'][0]) + message[0]) if m: merge_id = int(m.group(1)) mr = self._get_merge_request(target_project, merge_id) @@ -804,7 +804,7 @@ def create_proposal(self, description, reviewers=None, labels=None, merge_request = self.gl._create_mergerequest(**kwargs) except GitLabConflict as e: self.gl._handle_merge_request_conflict( - e.reason, self.source_branch.user_url, + e.message, self.source_branch.user_url, target_project['path_with_namespace']) except GitLabUnprocessable as e: if e.error == [ From b789452aa6627cfaeb7d7579ef7316e38d5604d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 21 Mar 2021 18:02:52 +0000 Subject: [PATCH 21/24] reason, not message. --- breezy/plugins/gitlab/hoster.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/breezy/plugins/gitlab/hoster.py b/breezy/plugins/gitlab/hoster.py index 5bfdd8d252..2186bb5ed2 100644 --- a/breezy/plugins/gitlab/hoster.py +++ b/breezy/plugins/gitlab/hoster.py @@ -113,11 +113,11 @@ def __init__(self, error): class GitLabConflict(errors.BzrError): - _fmt = "Conflict during operation: %(message)s" + _fmt = "Conflict during operation: %(reason)s" - def __init__(self, message): + def __init__(self, reason): errors.BzrError(self) - self.message = message + self.reason = reason class ForkingDisabled(errors.BzrError): @@ -238,7 +238,7 @@ def _update(self, **kwargs): self._mr['project_id'], self._mr['iid'], kwargs) except GitLabConflict as e: self.gl._handle_merge_request_conflict( - e.message, self.get_source_branch_url(), + e.reason, self.get_source_branch_url(), self._mr['target_project_id']) def __repr__(self): @@ -804,7 +804,7 @@ def create_proposal(self, description, reviewers=None, labels=None, merge_request = self.gl._create_mergerequest(**kwargs) except GitLabConflict as e: self.gl._handle_merge_request_conflict( - e.message, self.source_branch.user_url, + e.reason, self.source_branch.user_url, target_project['path_with_namespace']) except GitLabUnprocessable as e: if e.error == [ From 7f82d0e5425dd8882acaa02e5592f319b01c17c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 22 Mar 2021 03:45:38 +0000 Subject: [PATCH 22/24] Simplify subpath handling. --- breezy/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/workspace.py b/breezy/workspace.py index b38c34e73b..a970f18ef2 100644 --- a/breezy/workspace.py +++ b/breezy/workspace.py @@ -52,7 +52,7 @@ def reset_tree(local_tree, subpath=''): subpath: Subpath to operate on """ revert(local_tree, local_tree.branch.basis_tree(), - [subpath] if subpath not in ('.', '') else None) + [subpath] if subpath else None) deletables = list(iter_deletables( local_tree, unknown=True, ignored=False, detritus=False)) delete_items(deletables) From 8287d7988e7971c51b9ddc3e0c1d5b9113f2c1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 22 Mar 2021 03:46:01 +0000 Subject: [PATCH 23/24] Support leaving out filenames to revert(). --- breezy/transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breezy/transform.py b/breezy/transform.py index 3c7eee5f1d..19bec165dc 100644 --- a/breezy/transform.py +++ b/breezy/transform.py @@ -736,7 +736,7 @@ def _prepare_revert_transform(es, working_tree, target_tree, tt, filenames, return conflicts, merge_modified -def revert(working_tree, target_tree, filenames, backups=False, +def revert(working_tree, target_tree, filenames=None, backups=False, pb=None, change_reporter=None, merge_modified=None, basis_tree=None): """Revert a working tree's contents to those of a target tree.""" with cleanup.ExitStack() as es: From 1f958272e107a723250db42ed9a93ad429761a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 22 Mar 2021 03:46:11 +0000 Subject: [PATCH 24/24] Fix revert for git. --- breezy/git/tests/test_transform.py | 15 ++++++++++++++- breezy/git/transform.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/breezy/git/tests/test_transform.py b/breezy/git/tests/test_transform.py index 480ee30b05..4b14946169 100644 --- a/breezy/git/tests/test_transform.py +++ b/breezy/git/tests/test_transform.py @@ -20,7 +20,7 @@ import os -from ...transform import ROOT_PARENT, conflict_pass, resolve_conflicts +from ...transform import ROOT_PARENT, conflict_pass, resolve_conflicts, revert from . import TestCaseWithTransport @@ -39,3 +39,16 @@ def test_directory_exists(self): self.assertEqual([], list(conflicts)) tt.apply() self.assertEqual(set(['name1', 'name2']), set(os.listdir('dir'))) + + def test_revert_does_not_remove(self): + tree = self.make_branch_and_tree('.', format='git') + tt = tree.transform() + dir1 = tt.new_directory('dir', ROOT_PARENT) + tid = tt.new_file('name1', dir1, [b'content1']) + tt.version_file(tid) + tt.apply() + tree.commit('start') + with open('dir/name1', 'wb') as f: + f.write(b'new content2') + revert(tree, tree.basis_tree()) + self.assertEqual([], list(tree.iter_changes(tree.basis_tree()))) diff --git a/breezy/git/transform.py b/breezy/git/transform.py index ce09b9a7b7..3c0059885e 100644 --- a/breezy/git/transform.py +++ b/breezy/git/transform.py @@ -1441,7 +1441,7 @@ def _generate_index_changes(self): changes = {} changed_ids = set() for id_set in [self._new_name, self._new_parent, - self._new_executability]: + self._new_executability, self._new_contents]: changed_ids.update(id_set) for id_set in [self._new_name, self._new_parent]: removed_id.update(id_set)