Merge lp:~jelmer/brz/merge-3.1 into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/merge-3.1
Merge into: lp:brz
Diff against target: 1001 lines (+319/-93)
27 files modified
SECURITY.md (+12/-0)
breezy/builtins.py (+2/-2)
breezy/bzr/workingtree.py (+3/-3)
breezy/git/branch.py (+20/-7)
breezy/git/interrepo.py (+2/-2)
breezy/git/mapping.py (+2/-0)
breezy/git/remote.py (+1/-1)
breezy/git/tests/test_mapping.py (+20/-0)
breezy/git/tests/test_transform.py (+14/-1)
breezy/git/transform.py (+1/-1)
breezy/git/workingtree.py (+3/-3)
breezy/merge.py (+1/-1)
breezy/plugins/github/hoster.py (+17/-3)
breezy/plugins/gitlab/hoster.py (+58/-23)
breezy/plugins/launchpad/hoster.py (+2/-1)
breezy/plugins/pypi/__init__.py (+29/-0)
breezy/plugins/pypi/directory.py (+78/-0)
breezy/plugins/quilt/tests/test_merge.py (+2/-2)
breezy/propose.py (+1/-1)
breezy/tests/per_workingtree/test_commit.py (+3/-3)
breezy/tests/per_workingtree/test_merge_from_branch.py (+9/-9)
breezy/tests/per_workingtree/test_unversion.py (+3/-3)
breezy/tests/test_merge.py (+18/-18)
breezy/tests/test_merge_core.py (+9/-7)
breezy/transform.py (+1/-1)
breezy/workspace.py (+1/-1)
doc/en/release-notes/brz-3.1.txt (+7/-0)
To merge this branch: bzr merge lp:~jelmer/brz/merge-3.1
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+400002@code.launchpad.net

Commit message

Merge lp:brz/3.1.

Description of the change

Merge lp:brz/3.1.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'SECURITY.md'
2--- SECURITY.md 1970-01-01 00:00:00 +0000
3+++ SECURITY.md 2021-03-22 21:17:36 +0000
4@@ -0,0 +1,12 @@
5+# Security Policy
6+
7+## Supported Versions
8+
9+| Version | Supported |
10+| -------- | ------------------ |
11+| 3.1.x | :white_check_mark: |
12+| 3.0.x | :x: |
13+
14+## Reporting a Vulnerability
15+
16+Please report security issues by e-mail to breezy-core@googlegroups.com.
17
18=== modified file 'breezy/builtins.py'
19--- breezy/builtins.py 2020-08-10 15:00:17 +0000
20+++ breezy/builtins.py 2021-03-22 21:17:36 +0000
21@@ -4584,7 +4584,7 @@
22
23 def _do_merge(self, merger, change_reporter, allow_pending, verified):
24 merger.change_reporter = change_reporter
25- conflict_count = merger.do_merge()
26+ conflict_count = len(merger.do_merge())
27 if allow_pending:
28 merger.set_pending()
29 if verified == 'failed':
30@@ -4842,7 +4842,7 @@
31 conflicts = merger.do_merge()
32 finally:
33 tree.set_parent_ids(parents)
34- if conflicts > 0:
35+ if len(conflicts) > 0:
36 return 1
37 else:
38 return 0
39
40=== modified file 'breezy/bzr/workingtree.py'
41--- breezy/bzr/workingtree.py 2020-11-18 02:15:43 +0000
42+++ breezy/bzr/workingtree.py 2021-03-22 21:17:36 +0000
43@@ -1932,7 +1932,7 @@
44 # local work is unreferenced and will appear to have been lost.
45 #
46 with self.lock_tree_write():
47- nb_conflicts = 0
48+ nb_conflicts = []
49 try:
50 last_rev = self.get_parent_ids()[0]
51 except IndexError:
52@@ -1953,7 +1953,7 @@
53 show_base=show_base)
54 if nb_conflicts:
55 self.add_parent_tree((old_tip, other_tree))
56- return nb_conflicts
57+ return len(nb_conflicts)
58
59 if last_rev != _mod_revision.ensure_null(revision):
60 # the working tree is up to date with the branch
61@@ -1995,7 +1995,7 @@
62 (old_tip, self.branch.repository.revision_tree(old_tip)))
63 self.set_parent_trees(parent_trees)
64 last_rev = parent_trees[0][0]
65- return nb_conflicts
66+ return len(nb_conflicts)
67
68
69 class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
70
71=== modified file 'breezy/git/branch.py'
72--- breezy/git/branch.py 2021-01-10 00:25:52 +0000
73+++ breezy/git/branch.py 2021-03-22 21:17:36 +0000
74@@ -1450,13 +1450,26 @@
75 for k, v in self.source.tags.get_tag_dict().items():
76 ret.append((None, v))
77 ret.append((None, stop_revision))
78- try:
79- revidmap = self.interrepo.fetch_revs(ret, lossy=lossy, limit=limit)
80- except NoPushSupport:
81- raise errors.NoRoundtrippingSupport(self.source, self.target)
82- return _mod_repository.FetchResult(revidmap={
83- old_revid: new_revid
84- for (old_revid, (new_sha, new_revid)) in revidmap.items()})
85+ if getattr(self.interrepo, 'fetch_revs', None):
86+ try:
87+ revidmap = self.interrepo.fetch_revs(ret, lossy=lossy, limit=limit)
88+ except NoPushSupport:
89+ raise errors.NoRoundtrippingSupport(self.source, self.target)
90+ return _mod_repository.FetchResult(revidmap={
91+ old_revid: new_revid
92+ for (old_revid, (new_sha, new_revid)) in revidmap.items()})
93+ else:
94+ def determine_wants(refs):
95+ wants = []
96+ for git_sha, revid in ret:
97+ if git_sha is None:
98+ git_sha, mapping = self.target.lookup_bzr_revision_id(revid)
99+ wants.append(git_sha)
100+ return wants
101+
102+ self.interrepo.fetch_objects(
103+ determine_wants, lossy=lossy, limit=limit)
104+ return _mod_repository.FetchResult()
105
106 def pull(self, overwrite=False, stop_revision=None, local=False,
107 possible_transports=None, run_hooks=True, _stop_revno=None,
108
109=== modified file 'breezy/git/interrepo.py'
110--- breezy/git/interrepo.py 2021-01-10 00:25:52 +0000
111+++ breezy/git/interrepo.py 2021-03-22 21:17:36 +0000
112@@ -801,9 +801,9 @@
113 def git_update_refs(old_refs):
114 ret = {}
115 self.old_refs = {
116- k: (v, None) for (k, v) in viewitems(old_refs)}
117+ k: (v, None) for (k, v) in old_refs.items()}
118 new_refs = update_refs(self.old_refs)
119- for name, (gitid, revid) in viewitems(new_refs):
120+ for name, (gitid, revid) in new_refs.items():
121 if gitid is None:
122 gitid = self.source_store._lookup_revision_sha1(revid)
123 if not overwrite:
124
125=== modified file 'breezy/git/mapping.py'
126--- breezy/git/mapping.py 2020-08-22 22:46:24 +0000
127+++ breezy/git/mapping.py 2021-03-22 21:17:36 +0000
128@@ -411,9 +411,11 @@
129 rev.properties[u'author'] = commit.author.decode(encoding)
130 rev.message, rev.git_metadata = self._decode_commit_message(
131 rev, commit.message, encoding)
132+
133 if commit.encoding is not None:
134 rev.properties[u'git-explicit-encoding'] = commit.encoding.decode(
135 'ascii')
136+ if commit.encoding is not None and commit.encoding != b'false':
137 decode_using_encoding(rev, commit, commit.encoding.decode('ascii'))
138 else:
139 for encoding in ('utf-8', 'latin1'):
140
141=== modified file 'breezy/git/remote.py'
142--- breezy/git/remote.py 2020-08-10 15:00:17 +0000
143+++ breezy/git/remote.py 2021-03-22 21:17:36 +0000
144@@ -630,8 +630,8 @@
145 except errors.NoSuchRevision:
146 raise errors.NoRoundtrippingSupport(
147 source, self.open_branch(name=name, nascent_ok=True))
148+ old_sha = remote_refs.get(actual_refname)
149 if not overwrite:
150- old_sha = remote_refs.get(actual_refname)
151 if remote_divergence(old_sha, new_sha, source_store):
152 raise DivergedBranches(
153 source, self.open_branch(name, nascent_ok=True))
154
155=== modified file 'breezy/git/tests/test_mapping.py'
156--- breezy/git/tests/test_mapping.py 2020-06-10 23:47:24 +0000
157+++ breezy/git/tests/test_mapping.py 2021-03-22 21:17:36 +0000
158@@ -140,6 +140,26 @@
159 self.assertEqual("iso8859-1", rev.properties[u"git-explicit-encoding"])
160 self.assertTrue(u"git-implicit-encoding" not in rev.properties)
161
162+ def test_explicit_encoding_false(self):
163+ c = Commit()
164+ c.tree = b"cc9462f7f8263ef5adfbeff2fb936bb36b504cba"
165+ c.message = b"Some message"
166+ c.committer = b"Committer"
167+ c.commit_time = 4
168+ c.author_time = 5
169+ c.commit_timezone = 60 * 5
170+ c.author_timezone = 60 * 3
171+ c.author = u"Authér".encode("utf-8")
172+ c.encoding = b"false"
173+ mapping = BzrGitMappingv1()
174+ rev, roundtrip_revid, verifiers = mapping.import_commit(
175+ c, mapping.revision_id_foreign_to_bzr)
176+ self.assertEqual(None, roundtrip_revid)
177+ self.assertEqual({}, verifiers)
178+ self.assertEqual(u"Authér", rev.properties[u'author'])
179+ self.assertEqual("false", rev.properties[u"git-explicit-encoding"])
180+ self.assertTrue(u"git-implicit-encoding" not in rev.properties)
181+
182 def test_implicit_encoding_fallback(self):
183 c = Commit()
184 c.tree = b"cc9462f7f8263ef5adfbeff2fb936bb36b504cba"
185
186=== modified file 'breezy/git/tests/test_transform.py'
187--- breezy/git/tests/test_transform.py 2020-09-02 16:35:18 +0000
188+++ breezy/git/tests/test_transform.py 2021-03-22 21:17:36 +0000
189@@ -20,7 +20,7 @@
190
191 import os
192
193-from ...transform import ROOT_PARENT, conflict_pass, resolve_conflicts
194+from ...transform import ROOT_PARENT, conflict_pass, resolve_conflicts, revert
195 from . import TestCaseWithTransport
196
197
198@@ -39,3 +39,16 @@
199 self.assertEqual([], list(conflicts))
200 tt.apply()
201 self.assertEqual(set(['name1', 'name2']), set(os.listdir('dir')))
202+
203+ def test_revert_does_not_remove(self):
204+ tree = self.make_branch_and_tree('.', format='git')
205+ tt = tree.transform()
206+ dir1 = tt.new_directory('dir', ROOT_PARENT)
207+ tid = tt.new_file('name1', dir1, [b'content1'])
208+ tt.version_file(tid)
209+ tt.apply()
210+ tree.commit('start')
211+ with open('dir/name1', 'wb') as f:
212+ f.write(b'new content2')
213+ revert(tree, tree.basis_tree())
214+ self.assertEqual([], list(tree.iter_changes(tree.basis_tree())))
215
216=== modified file 'breezy/git/transform.py'
217--- breezy/git/transform.py 2020-11-19 17:40:49 +0000
218+++ breezy/git/transform.py 2021-03-22 21:17:36 +0000
219@@ -1440,7 +1440,7 @@
220 changes = {}
221 changed_ids = set()
222 for id_set in [self._new_name, self._new_parent,
223- self._new_executability]:
224+ self._new_executability, self._new_contents]:
225 changed_ids.update(id_set)
226 for id_set in [self._new_name, self._new_parent]:
227 removed_id.update(id_set)
228
229=== modified file 'breezy/git/workingtree.py'
230--- breezy/git/workingtree.py 2020-11-18 02:15:43 +0000
231+++ breezy/git/workingtree.py 2021-03-22 21:17:36 +0000
232@@ -1518,7 +1518,7 @@
233 #
234 with self.lock_tree_write():
235 from .. import merge
236- nb_conflicts = 0
237+ nb_conflicts = []
238 try:
239 last_rev = self.get_parent_ids()[0]
240 except IndexError:
241@@ -1539,7 +1539,7 @@
242 show_base=show_base)
243 if nb_conflicts:
244 self.add_parent_tree((old_tip, other_tree))
245- return nb_conflicts
246+ return len(nb_conflicts)
247
248 if last_rev != _mod_revision.ensure_null(revision):
249 to_tree = self.branch.repository.revision_tree(revision)
250@@ -1572,7 +1572,7 @@
251 (old_tip, self.branch.repository.revision_tree(old_tip)))
252 self.set_parent_trees(parent_trees)
253 last_rev = parent_trees[0][0]
254- return nb_conflicts
255+ return len(nb_conflicts)
256
257
258 class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
259
260=== modified file 'breezy/merge.py'
261--- breezy/merge.py 2020-08-22 22:46:24 +0000
262+++ breezy/merge.py 2021-03-22 21:17:36 +0000
263@@ -666,7 +666,7 @@
264 trace.note(gettext("%d conflicts encountered.")
265 % len(merge.cooked_conflicts))
266
267- return len(merge.cooked_conflicts)
268+ return merge.cooked_conflicts
269
270
271 class _InventoryNoneEntry(object):
272
273=== modified file 'breezy/plugins/github/hoster.py'
274--- breezy/plugins/github/hoster.py 2020-11-18 02:15:43 +0000
275+++ breezy/plugins/github/hoster.py 2021-03-22 21:17:36 +0000
276@@ -456,7 +456,7 @@
277 repo = self._get_repo(owner, project)
278 return github_url_to_bzr_url(repo['ssh_url'], branch_name)
279
280- def get_derived_branch(self, base_branch, name, project=None, owner=None):
281+ def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None):
282 base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
283 base_repo = self._get_repo(base_owner, base_project)
284 if owner is None:
285@@ -465,10 +465,24 @@
286 project = base_repo['name']
287 try:
288 remote_repo = self._get_repo(owner, project)
289- full_url = github_url_to_bzr_url(remote_repo['ssh_url'], name)
290- return _mod_branch.Branch.open(full_url)
291 except NoSuchProject:
292 raise errors.NotBranchError('%s/%s/%s' % (WEB_GITHUB_URL, owner, project))
293+ if preferred_schemes is None:
294+ preferred_schemes = ['git+ssh']
295+ for scheme in preferred_schemes:
296+ if scheme == 'git+ssh':
297+ github_url = remote_repo['ssh_url']
298+ break
299+ if scheme == 'https':
300+ github_url = remote_repo['clone_url']
301+ break
302+ if scheme == 'git':
303+ github_url = remote_repo['git_url']
304+ break
305+ else:
306+ raise AssertionError
307+ full_url = github_url_to_bzr_url(github_url, name)
308+ return _mod_branch.Branch.open(full_url)
309
310 def get_proposer(self, source_branch, target_branch):
311 return GitHubMergeProposalBuilder(self, source_branch, target_branch)
312
313=== modified file 'breezy/plugins/gitlab/hoster.py'
314--- breezy/plugins/gitlab/hoster.py 2020-11-18 02:15:43 +0000
315+++ breezy/plugins/gitlab/hoster.py 2021-03-22 21:17:36 +0000
316@@ -81,7 +81,8 @@
317 _fmt = "GitLab can not process request: %(error)s."
318
319 def __init__(self, error):
320- errors.BzrError.__init__(self, error=error)
321+ errors.BzrError.__init__(self)
322+ self.error = error
323
324
325 class DifferentGitLabInstances(errors.BzrError):
326@@ -112,7 +113,8 @@
327 _fmt = "Conflict during operation: %(reason)s"
328
329 def __init__(self, reason):
330- errors.BzrError(self, reason=reason)
331+ errors.BzrError(self)
332+ self.reason = reason
333
334
335 class ForkingDisabled(errors.BzrError):
336@@ -228,7 +230,13 @@
337 self._mr = mr
338
339 def _update(self, **kwargs):
340- self.gl._update_merge_request(self._mr['project_id'], self._mr['iid'], kwargs)
341+ try:
342+ self.gl._update_merge_request(
343+ self._mr['project_id'], self._mr['iid'], kwargs)
344+ except GitLabConflict as e:
345+ self.gl._handle_merge_request_conflict(
346+ e.reason, self.get_source_branch_url(),
347+ self._mr['target_project_id'])
348
349 def __repr__(self):
350 return "<%s at %r>" % (type(self).__name__, self._mr['web_url'])
351@@ -302,7 +310,7 @@
352 elif self._mr['merge_status'] == 'can_be_merged':
353 return True
354 elif self._mr['merge_status'] in (
355- 'unchecked', 'cannot_be_merged_recheck'):
356+ 'unchecked', 'cannot_be_merged_recheck', 'checking'):
357 # See https://gitlab.com/gitlab-org/gitlab/-/commit/7517105303c for
358 # an explanation of the distinction between unchecked and
359 # cannot_be_merged_recheck
360@@ -407,7 +415,17 @@
361 return json.loads(response.data)
362 _unexpected_status(path, response)
363
364- def _fork_project(self, project_name, timeout=50, interval=5, owner=None):
365+ def create_project(self, project_name):
366+ fields = {'name': project_name}
367+ response = self._api_request('POST', 'projects', fields=fields)
368+ if response.status == 403:
369+ raise errors.PermissionDenied(response.text)
370+ if response.status not in (200, 201):
371+ _unexpected_status('projects', response)
372+ project = json.loads(response.data)
373+ return project
374+
375+ def fork_project(self, project_name, timeout=50, interval=5, owner=None):
376 path = 'projects/%s/fork' % urlutils.quote(str(project_name), '')
377 fields = {}
378 if owner is not None:
379@@ -434,6 +452,18 @@
380 project = self._get_project(project['path_with_namespace'])
381 return project
382
383+ def _handle_merge_request_conflict(self, message, source_url, target_project):
384+ m = re.fullmatch(
385+ r'Another open merge request already exists for '
386+ r'this source branch: \!([0-9]+)',
387+ message[0])
388+ if m:
389+ merge_id = int(m.group(1))
390+ mr = self._get_merge_request(target_project, merge_id)
391+ raise MergeProposalExists(
392+ source_url, GitLabMergeProposal(self, mr))
393+ raise MergeRequestConflict(reason)
394+
395 def get_current_user(self):
396 return self._current_user['username']
397
398@@ -493,6 +523,8 @@
399 response = self._api_request('PUT', path, fields=mr)
400 if response.status == 200:
401 return json.loads(response.data)
402+ if response.status == 409:
403+ raise GitLabConflict(json.loads(response.data).get('message'))
404 if response.status == 403:
405 raise errors.PermissionDenied(response.text)
406 _unexpected_status(path, response)
407@@ -527,7 +559,7 @@
408 if response.status == 403:
409 raise errors.PermissionDenied(response.text)
410 if response.status == 409:
411- raise MergeRequestConflict(json.loads(response.data))
412+ raise GitLabConflict(json.loads(response.data).get('message'))
413 if response.status == 422:
414 data = json.loads(response.data)
415 raise GitLabUnprocessable(data['error'])
416@@ -555,7 +587,7 @@
417 try:
418 target_project = self._get_project('%s/%s' % (owner, project))
419 except NoSuchProject:
420- target_project = self._fork_project(
421+ target_project = self.fork_project(
422 base_project['path_with_namespace'], owner=owner)
423 remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo'])
424 remote_dir = controldir.ControlDir.open(remote_repo_url)
425@@ -573,7 +605,7 @@
426 target_project['http_url_to_repo'], name)
427 return push_result.target_branch, public_url
428
429- def get_derived_branch(self, base_branch, name, project=None, owner=None):
430+ def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None):
431 (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
432 if owner is None:
433 owner = self.get_current_user()
434@@ -583,8 +615,19 @@
435 target_project = self._get_project('%s/%s' % (owner, project))
436 except NoSuchProject:
437 raise errors.NotBranchError('%s/%s/%s' % (self.base_url, owner, project))
438- return _mod_branch.Branch.open(gitlab_url_to_bzr_url(
439- target_project['ssh_url_to_repo'], name))
440+ if preferred_schemes is None:
441+ preferred_schemes = ['git+ssh']
442+ for scheme in preferred_schemes:
443+ if scheme == 'git+ssh':
444+ gitlab_url = target_project['ssh_url_to_repo']
445+ break
446+ elif scheme == 'https':
447+ gitlab_url = target_project['http_url_to_repo']
448+ break
449+ else:
450+ raise AssertionError
451+ return _mod_branch.Branch.open(
452+ gitlab_url_to_bzr_url(gitlab_url, name))
453
454 def get_proposer(self, source_branch, target_branch):
455 return GitlabMergeProposalBuilder(self, source_branch, target_branch)
456@@ -657,7 +700,7 @@
457 yield GitLabMergeProposal(self, mp)
458
459 def iter_my_forks(self, owner=None):
460- if owner is not None:
461+ if owner is None:
462 owner = self.get_current_user()
463 for project in self._list_projects(owner=owner):
464 base_project = project.get('forked_from_project')
465@@ -754,18 +797,10 @@
466 kwargs['assignee_ids'].append(user['id'])
467 try:
468 merge_request = self.gl._create_mergerequest(**kwargs)
469- except MergeRequestConflict as e:
470- m = re.fullmatch(
471- r'Another open merge request already exists for '
472- r'this source branch: \!([0-9]+)',
473- e.reason['message'][0])
474- if m:
475- merge_id = int(m.group(1))
476- mr = self.gl._get_merge_request(
477- target_project['path_with_namespace'], merge_id)
478- raise MergeProposalExists(
479- self.source_branch.user_url, GitLabMergeProposal(self.gl, mr))
480- raise Exception('conflict: %r' % e.reason)
481+ except GitLabConflict as e:
482+ self.gl._handle_merge_request_conflict(
483+ e.reason, self.source_branch.user_url,
484+ target_project['path_with_namespace'])
485 except GitLabUnprocessable as e:
486 if e.error == [
487 "Source project is not a fork of the target project"]:
488
489=== modified file 'breezy/plugins/launchpad/hoster.py'
490--- breezy/plugins/launchpad/hoster.py 2020-11-18 02:15:43 +0000
491+++ breezy/plugins/launchpad/hoster.py 2021-03-22 21:17:36 +0000
492@@ -408,7 +408,8 @@
493 else:
494 raise AssertionError('not a valid Launchpad URL')
495
496- def get_derived_branch(self, base_branch, name, project=None, owner=None):
497+ def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None):
498+ # TODO(jelmer): honor preferred_schemes
499 if owner is None:
500 owner = self.launchpad.me.name
501 (base_vcs, base_user, base_password, base_path,
502
503=== added directory 'breezy/plugins/pypi'
504=== added file 'breezy/plugins/pypi/__init__.py'
505--- breezy/plugins/pypi/__init__.py 1970-01-01 00:00:00 +0000
506+++ breezy/plugins/pypi/__init__.py 2021-03-22 21:17:36 +0000
507@@ -0,0 +1,29 @@
508+# Copyright (C) 2021 Breezy Developers
509+#
510+# This program is free software; you can redistribute it and/or modify
511+# it under the terms of the GNU General Public License as published by
512+# the Free Software Foundation; either version 2 of the License, or
513+# (at your option) any later version.
514+#
515+# This program is distributed in the hope that it will be useful,
516+# but WITHOUT ANY WARRANTY; without even the implied warranty of
517+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
518+# GNU General Public License for more details.
519+#
520+# You should have received a copy of the GNU General Public License
521+# along with this program; if not, write to the Free Software
522+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
523+
524+"""Support for looking up URLs from pypi.
525+"""
526+
527+from __future__ import absolute_import
528+
529+from ... import (
530+ version_info, # noqa: F401
531+ )
532+from ...directory_service import directories
533+
534+directories.register_lazy('pypi:', __name__ + '.directory',
535+ 'PypiDirectory',
536+ 'Pypi-based directory service',)
537
538=== added file 'breezy/plugins/pypi/directory.py'
539--- breezy/plugins/pypi/directory.py 1970-01-01 00:00:00 +0000
540+++ breezy/plugins/pypi/directory.py 2021-03-22 21:17:36 +0000
541@@ -0,0 +1,78 @@
542+# Copyright (C) 2021 Breezy Developers
543+#
544+# This program is free software; you can redistribute it and/or modify
545+# it under the terms of the GNU General Public License as published by
546+# the Free Software Foundation; either version 2 of the License, or
547+# (at your option) any later version.
548+#
549+# This program is distributed in the hope that it will be useful,
550+# but WITHOUT ANY WARRANTY; without even the implied warranty of
551+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
552+# GNU General Public License for more details.
553+#
554+# You should have received a copy of the GNU General Public License
555+# along with this program; if not, write to the Free Software
556+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
557+
558+"""Directory lookup that uses pypi."""
559+
560+from __future__ import absolute_import
561+
562+from breezy.errors import BzrError
563+from breezy.trace import note
564+from breezy.urlutils import InvalidURL
565+
566+import json
567+
568+try:
569+ from urllib.request import urlopen
570+ from urllib.parse import urlparse
571+ from urllib.error import HTTPError
572+except ImportError: # python < 3
573+ from urllib import urlopen, HTTPError
574+ from urlparse import urlparse
575+
576+
577+class PypiProjectWithoutRepositoryURL(InvalidURL):
578+
579+ _fmt = "No repository URL set for pypi project %(name)s"
580+
581+ def __init__(self, name, url=None):
582+ BzrError.__init__(self, name=name, url=url)
583+
584+
585+class NoSuchPypiProject(InvalidURL):
586+
587+ _fmt = "No pypi project with name %(name)s"
588+
589+ def __init__(self, name, url=None):
590+ BzrError.__init__(self, name=name, url=url)
591+
592+
593+def find_repo_url(data):
594+ for key, value in data['info']['project_urls'].items():
595+ if key == 'Repository':
596+ note('Found repository URL %s for pypi project %s',
597+ value, name)
598+ return value
599+ parsed_url = urlparse(value)
600+ if (parsed_url.hostname == 'github.com' and
601+ parsed_url.path.strip('/').count('/') == 1):
602+ return value
603+
604+
605+class PypiDirectory(object):
606+
607+ def look_up(self, name, url, purpose=None):
608+ """See DirectoryService.look_up"""
609+ try:
610+ with urlopen('https://pypi.org/pypi/%s/json' % name) as f:
611+ data = json.load(f)
612+ except HTTPError as e:
613+ if e.status == 404:
614+ raise NoSuchPypiProject(name, url=url)
615+ raise
616+ url = find_repo_url(data)
617+ if url is None:
618+ raise PypiProjectWithoutRepositoryURL(name, url=url)
619+ return url
620
621=== modified file 'breezy/plugins/quilt/tests/test_merge.py'
622--- breezy/plugins/quilt/tests/test_merge.py 2020-02-18 01:57:45 +0000
623+++ breezy/plugins/quilt/tests/test_merge.py 2021-03-22 21:17:36 +0000
624@@ -149,7 +149,7 @@
625 """, "a/debian/patches/patch1")
626 # "a" should be unapplied again
627 self.assertPathDoesNotExist("a/a")
628- self.assertEquals(1, conflicts)
629+ self.assertEquals(1, len(conflicts))
630
631 def test_auto_apply_patches_after_checkout(self):
632 self.enable_hooks()
633@@ -286,7 +286,7 @@
634 c
635 >>>>>>> MERGE-SOURCE
636 """, "a/a")
637- self.assertEquals(2, conflicts)
638+ self.assertEquals(2, len(conflicts))
639
640
641
642
643=== modified file 'breezy/propose.py'
644--- breezy/propose.py 2020-11-18 02:15:43 +0000
645+++ breezy/propose.py 2021-03-22 21:17:36 +0000
646@@ -286,7 +286,7 @@
647 """
648 raise NotImplementedError(self.publish_derived)
649
650- def get_derived_branch(self, base_branch, name, project=None, owner=None):
651+ def get_derived_branch(self, base_branch, name, project=None, owner=None, preferred_schemes=None):
652 """Get a derived branch ('a fork').
653 """
654 raise NotImplementedError(self.get_derived_branch)
655
656=== modified file 'breezy/tests/per_workingtree/test_commit.py'
657--- breezy/tests/per_workingtree/test_commit.py 2020-08-15 22:46:49 +0000
658+++ breezy/tests/per_workingtree/test_commit.py 2021-03-22 21:17:36 +0000
659@@ -101,11 +101,11 @@
660
661 # Merging from A should introduce conflicts because 'n' was modified
662 # (in A) and removed (in B), so 'a' needs to be restored.
663- num_conflicts = tree_b.merge_from_branch(tree_a.branch)
664+ conflicts = tree_b.merge_from_branch(tree_a.branch)
665 if tree_b.has_versioned_directories():
666- self.assertEqual(3, num_conflicts)
667+ self.assertEqual(3, len(conflicts))
668 else:
669- self.assertEqual(2, num_conflicts)
670+ self.assertEqual(2, len(conflicts))
671
672 self.assertThat(
673 tree_b, HasPathRelations(
674
675=== modified file 'breezy/tests/per_workingtree/test_merge_from_branch.py'
676--- breezy/tests/per_workingtree/test_merge_from_branch.py 2020-08-15 22:46:49 +0000
677+++ breezy/tests/per_workingtree/test_merge_from_branch.py 2021-03-22 21:17:36 +0000
678@@ -224,9 +224,9 @@
679 outer.commit('delete file3')
680 nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[2])
681 if outer.supports_rename_tracking():
682- self.assertEqual(4, nb_conflicts)
683+ self.assertEqual(4, len(nb_conflicts))
684 else:
685- self.assertEqual(1, nb_conflicts)
686+ self.assertEqual(1, len(nb_conflicts))
687 self.assertTreeLayout(['dir-outer',
688 'dir-outer/dir',
689 'dir-outer/dir/file1',
690@@ -245,9 +245,9 @@
691 # file4 could not be added to its original root, so it gets added to
692 # the new root with a conflict.
693 if outer.supports_rename_tracking():
694- self.assertEqual(1, nb_conflicts)
695+ self.assertEqual(1, len(nb_conflicts))
696 else:
697- self.assertEqual(0, nb_conflicts)
698+ self.assertEqual(0, len(nb_conflicts))
699 self.assertTreeLayout(['dir-outer',
700 'dir-outer/dir',
701 'dir-outer/dir/file1',
702@@ -261,9 +261,9 @@
703 # 1 conflict, because file4 can't be put into the old root
704 nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[3])
705 if outer.supports_rename_tracking():
706- self.assertEqual(1, nb_conflicts)
707+ self.assertEqual(1, len(nb_conflicts))
708 else:
709- self.assertEqual(0, nb_conflicts)
710+ self.assertEqual(0, len(nb_conflicts))
711 try:
712 outer.set_conflicts([])
713 except errors.UnsupportedOperation:
714@@ -275,7 +275,7 @@
715 # And now file4 gets renamed into an existing dir
716 nb_conflicts = outer.merge_from_branch(inner, to_revision=revs[4])
717 if outer.supports_rename_tracking():
718- self.assertEqual(1, nb_conflicts)
719+ self.assertEqual(1, len(nb_conflicts))
720 self.assertTreeLayout(['dir-outer',
721 'dir-outer/dir',
722 'dir-outer/dir/file1',
723@@ -285,9 +285,9 @@
724 outer)
725 else:
726 if outer.has_versioned_directories():
727- self.assertEqual(2, nb_conflicts)
728+ self.assertEqual(2, len(nb_conflicts))
729 else:
730- self.assertEqual(1, nb_conflicts)
731+ self.assertEqual(1, len(nb_conflicts))
732 self.assertTreeLayout(['dir',
733 'dir-outer',
734 'dir-outer/dir',
735
736=== modified file 'breezy/tests/per_workingtree/test_unversion.py'
737--- breezy/tests/per_workingtree/test_unversion.py 2020-08-15 17:47:31 +0000
738+++ breezy/tests/per_workingtree/test_unversion.py 2021-03-22 21:17:36 +0000
739@@ -175,11 +175,11 @@
740 # Merging from A should introduce conflicts because 'n' was modified
741 # and removed, so 'a' needs to be restored. We also have a conflict
742 # because 'a' is still an existing directory
743- num_conflicts = tree_b.merge_from_branch(tree_a.branch)
744+ conflicts = tree_b.merge_from_branch(tree_a.branch)
745 if tree_b.has_versioned_directories():
746- self.assertEqual(4, num_conflicts)
747+ self.assertEqual(4, len(conflicts))
748 else:
749- self.assertEqual(1, num_conflicts)
750+ self.assertEqual(1, len(conflicts))
751
752 self.assertThat(
753 tree_b,
754
755=== modified file 'breezy/tests/test_merge.py'
756--- breezy/tests/test_merge.py 2020-08-22 22:46:24 +0000
757+++ breezy/tests/test_merge.py 2021-03-22 21:17:36 +0000
758@@ -434,8 +434,8 @@
759 first_rev)
760 merger.merge_type = _mod_merge.Merge3Merger
761 merger.interesting_files = 'a'
762- conflict_count = merger.do_merge()
763- self.assertEqual(0, conflict_count)
764+ conflicts = merger.do_merge()
765+ self.assertEqual([], conflicts)
766
767 self.assertPathDoesNotExist("a")
768 tree.revert()
769@@ -515,8 +515,8 @@
770 _mod_revision.NULL_REVISION,
771 first_rev)
772 merger.merge_type = _mod_merge.Merge3Merger
773- conflict_count = merger.do_merge()
774- self.assertEqual(0, conflict_count)
775+ conflicts = merger.do_merge()
776+ self.assertEqual([], conflicts)
777 self.assertEqual({''}, set(tree.all_versioned_paths()))
778 tree.set_parent_ids([])
779
780@@ -2190,7 +2190,7 @@
781 [('modify', ('a', b'a\nb\nc\nd\ne\nf\n'))],
782 revision_id=b'D-id')
783 wt, conflicts = self.do_merge(builder, b'E-id')
784- self.assertEqual(0, conflicts)
785+ self.assertEqual([], conflicts)
786 # The merge should have simply update the contents of 'a'
787 self.assertEqual(b'a\nb\nc\nd\ne\nf\n', wt.get_file_text('a'))
788
789@@ -2220,7 +2220,7 @@
790 [('rename', ('bar', 'baz'))], revision_id=b'F-id')
791 builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
792 wt, conflicts = self.do_merge(builder, b'F-id')
793- self.assertEqual(0, conflicts)
794+ self.assertEqual([], conflicts)
795 # The merge should simply recognize that the final rename takes
796 # precedence
797 self.assertEqual('baz', wt.id2path(b'foo-id'))
798@@ -2251,7 +2251,7 @@
799 [('unversion', 'bar')], revision_id=b'F-id')
800 builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
801 wt, conflicts = self.do_merge(builder, b'F-id')
802- self.assertEqual(0, conflicts)
803+ self.assertEqual([], conflicts)
804 self.assertRaises(errors.NoSuchId, wt.id2path, b'foo-id')
805
806 def test_executable_changes(self):
807@@ -2286,7 +2286,7 @@
808 wt.revert()
809 self.assertFalse(wt.is_executable('foo'))
810 conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
811- self.assertEqual(0, conflicts)
812+ self.assertEqual(0, len(conflicts))
813 self.assertTrue(wt.is_executable('foo'))
814
815 def test_create_symlink(self):
816@@ -2322,7 +2322,7 @@
817 wt.revert()
818 self.assertFalse(wt.is_versioned('foo'))
819 conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
820- self.assertEqual(0, conflicts)
821+ self.assertEqual(0, len(conflicts))
822 self.assertEqual(b'foo-id', wt.path2id('foo'))
823 self.assertEqual('bar', wt.get_symlink_target('foo'))
824
825@@ -2352,7 +2352,7 @@
826 builder.build_snapshot([b'B-id', b'C-id'], [],
827 revision_id=b'D-id')
828 wt, conflicts = self.do_merge(builder, b'E-id')
829- self.assertEqual(1, conflicts)
830+ self.assertEqual(1, len(conflicts))
831 self.assertEqualDiff(b'<<<<<<< TREE\n'
832 b'B content\n'
833 b'=======\n'
834@@ -2402,7 +2402,7 @@
835 wt.merge_from_branch(wt.branch, b'C-id')
836 wt.commit('D merges B & C', rev_id=b'D-id')
837 conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
838- self.assertEqual(0, conflicts)
839+ self.assertEqual(0, len(conflicts))
840 self.assertEqual('bing', wt.get_symlink_target('foo'))
841
842 def test_renamed_symlink(self):
843@@ -2459,7 +2459,7 @@
844 False),
845 ], entries)
846 conflicts = wt.merge_from_branch(wt.branch, to_revision=b'F-id')
847- self.assertEqual(0, conflicts)
848+ self.assertEqual(0, len(conflicts))
849 self.assertEqual('blah', wt.id2path(b'foo-id'))
850
851 def test_symlink_no_content_change(self):
852@@ -2509,7 +2509,7 @@
853 self.assertEqual([], list(merge_obj._entries_lca()))
854 # Now do a real merge, just to test the rest of the stack
855 conflicts = wt.merge_from_branch(wt.branch, to_revision=b'E-id')
856- self.assertEqual(0, conflicts)
857+ self.assertEqual(0, len(conflicts))
858 self.assertEqual('bing', wt.get_symlink_target('foo'))
859
860 def test_symlink_this_changed_kind(self):
861@@ -2652,7 +2652,7 @@
862 [('rename', ('bar', 'foo'))], revision_id=b'F-id') # Rename back to BASE
863 builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
864 wt, conflicts = self.do_merge(builder, b'F-id')
865- self.assertEqual(0, conflicts)
866+ self.assertEqual([], conflicts)
867 self.assertEqual('foo', wt.id2path(b'foo-id'))
868
869 def test_other_reverted_content_to_base(self):
870@@ -2673,7 +2673,7 @@
871 revision_id=b'F-id') # Revert back to BASE
872 builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
873 wt, conflicts = self.do_merge(builder, b'F-id')
874- self.assertEqual(0, conflicts)
875+ self.assertEqual([], conflicts)
876 # TODO: We need to use the per-file graph to properly select a BASE
877 # before this will work. Or at least use the LCA trees to find
878 # the appropriate content base. (which is B, not A).
879@@ -2697,7 +2697,7 @@
880 revision_id=b'F-id') # Override B content
881 builder.build_snapshot([b'B-id', b'C-id'], [], revision_id=b'D-id')
882 wt, conflicts = self.do_merge(builder, b'F-id')
883- self.assertEqual(0, conflicts)
884+ self.assertEqual([], conflicts)
885 self.assertEqual(b'F content\n', wt.get_file_text('foo'))
886
887 def test_all_wt(self):
888@@ -3302,7 +3302,7 @@
889 dest_wt = self.setup_simple_branch('dest', ['dir/', 'dir/file.txt'])
890 self.setup_simple_branch('src', ['README'])
891 conflicts = self.do_merge_into('src', 'dest/dir')
892- self.assertEqual(1, conflicts)
893+ self.assertEqual(1, len(conflicts))
894 dest_wt.lock_read()
895 self.addCleanup(dest_wt.unlock)
896 # The r1-lib1 revision should be merged into this one
897@@ -3330,7 +3330,7 @@
898 # This is an edge case that shouldn't happen to users very often. So
899 # we don't care really about the exact presentation of the conflict,
900 # just that there is one.
901- self.assertEqual(1, conflicts)
902+ self.assertEqual(1, len(conflicts))
903
904 def test_only_subdir(self):
905 """When the location points to just part of a tree, merge just that
906
907=== modified file 'breezy/tests/test_merge_core.py'
908--- breezy/tests/test_merge_core.py 2020-08-09 18:10:01 +0000
909+++ breezy/tests/test_merge_core.py 2021-03-22 21:17:36 +0000
910@@ -503,13 +503,13 @@
911 self.build_tree_contents([('b/file', b'this contents contents\n')])
912 wtb = d_b.open_workingtree()
913 wtb.commit('this revision', allow_pointless=False)
914- self.assertEqual(1, wtb.merge_from_branch(wta.branch))
915+ self.assertEqual(1, len(wtb.merge_from_branch(wta.branch)))
916 self.assertPathExists('b/file.THIS')
917 self.assertPathExists('b/file.BASE')
918 self.assertPathExists('b/file.OTHER')
919 wtb.revert()
920- self.assertEqual(1, wtb.merge_from_branch(wta.branch,
921- merge_type=WeaveMerger))
922+ self.assertEqual(1, len(wtb.merge_from_branch(wta.branch,
923+ merge_type=WeaveMerger)))
924 self.assertPathExists('b/file')
925 self.assertPathExists('b/file.THIS')
926 self.assertPathExists('b/file.BASE')
927@@ -544,9 +544,9 @@
928 revision_id=b'E-id')
929 builder.finish_series()
930 tree = builder.get_branch().create_checkout('tree', lightweight=True)
931- self.assertEqual(1, tree.merge_from_branch(tree.branch,
932+ self.assertEqual(1, len(tree.merge_from_branch(tree.branch,
933 to_revision=b'D-id',
934- merge_type=WeaveMerger))
935+ merge_type=WeaveMerger)))
936 self.assertPathExists('tree/foo.THIS')
937 self.assertPathExists('tree/foo.OTHER')
938 self.expectFailure('fail to create .BASE in some criss-cross merges',
939@@ -640,8 +640,10 @@
940 b_wt.rename_one('deux', 'un')
941 b_wt.rename_one('tmp', 'deux')
942 b_wt.commit('r1', rev_id=b'r1')
943- self.assertEqual(0, a_wt.merge_from_branch(b_wt.branch,
944- b_wt.branch.last_revision(), b_wt.branch.get_rev_id(1)))
945+ self.assertEqual(
946+ 0, len(a_wt.merge_from_branch(
947+ b_wt.branch, b_wt.branch.last_revision(),
948+ b_wt.branch.get_rev_id(1))))
949 self.assertPathExists('a/un')
950 self.assertTrue('a/deux')
951 self.assertFalse(os.path.exists('a/tmp'))
952
953=== modified file 'breezy/transform.py'
954--- breezy/transform.py 2020-11-19 18:28:52 +0000
955+++ breezy/transform.py 2021-03-22 21:17:36 +0000
956@@ -729,7 +729,7 @@
957 return conflicts, merge_modified
958
959
960-def revert(working_tree, target_tree, filenames, backups=False,
961+def revert(working_tree, target_tree, filenames=None, backups=False,
962 pb=None, change_reporter=None, merge_modified=None, basis_tree=None):
963 """Revert a working tree's contents to those of a target tree."""
964 with contextlib.ExitStack() as es:
965
966=== modified file 'breezy/workspace.py'
967--- breezy/workspace.py 2020-08-06 22:27:46 +0000
968+++ breezy/workspace.py 2021-03-22 21:17:36 +0000
969@@ -52,7 +52,7 @@
970 subpath: Subpath to operate on
971 """
972 revert(local_tree, local_tree.branch.basis_tree(),
973- [subpath] if subpath not in ('.', '') else None)
974+ [subpath] if subpath else None)
975 deletables = list(iter_deletables(
976 local_tree, unknown=True, ignored=False, detritus=False))
977 delete_items(deletables)
978
979=== modified file 'doc/en/release-notes/brz-3.1.txt'
980--- doc/en/release-notes/brz-3.1.txt 2020-11-22 15:50:02 +0000
981+++ doc/en/release-notes/brz-3.1.txt 2021-03-22 21:17:36 +0000
982@@ -54,6 +54,9 @@
983 but it prevents the DWIM revision specifier from treating "svn:"
984 as a URL. (Jelmer Vernooij)
985
986+ * New `pypi` directory that can be used to access remote repositories
987+ declared in pypi. (Jelmer Vernooij)
988+
989 Bug Fixes
990 *********
991
992@@ -96,6 +99,10 @@
993 * File ids are no longer returned in ``Tree.walkdirs``.
994 (Jelmer Vernooij)
995
996+ * ``WorkingTree.merge_from_branch``, ``Merge.do_merge`` and
997+ ``merge_inner`` now return a list of conflicts rather than number of
998+ conflicts. (Jelmer Vernooij)
999+
1000 Internals
1001 *********
1002

Subscribers

People subscribed via source and target branches