Merge lp:~jelmer/brz/trunk-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/trunk-3.1
Merge into: lp:brz
Diff against target: 1889 lines (+542/-334)
25 files modified
breezy/branch.py (+42/-26)
breezy/bzr/branch.py (+3/-1)
breezy/bzr/bzrdir.py (+4/-2)
breezy/bzr/knitpack_repo.py (+0/-6)
breezy/bzr/remote.py (+2/-2)
breezy/bzr/tag.py (+113/-0)
breezy/controldir.py (+15/-8)
breezy/git/branch.py (+152/-90)
breezy/git/dir.py (+5/-4)
breezy/git/interrepo.py (+12/-6)
breezy/git/workingtree.py (+2/-2)
breezy/plugins/propose/github.py (+6/-4)
breezy/plugins/propose/gitlabs.py (+3/-3)
breezy/plugins/propose/launchpad.py (+15/-11)
breezy/plugins/weave_fmt/bzrdir.py (+2/-2)
breezy/propose.py (+1/-1)
breezy/tag.py (+80/-160)
breezy/tests/per_branch/test_pull.py (+1/-1)
breezy/tests/per_branch/test_tags.py (+15/-0)
breezy/tests/per_controldir/test_push.py (+11/-0)
breezy/tests/per_interbranch/test_pull.py (+21/-1)
breezy/tests/per_interbranch/test_push.py (+20/-0)
breezy/tests/test_foreign.py (+1/-1)
breezy/tests/test_tag.py (+14/-1)
breezy/workingtree.py (+2/-2)
To merge this branch: bzr merge lp:~jelmer/brz/trunk-3.1
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+379600@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=== modified file 'breezy/branch.py'
2--- breezy/branch.py 2020-02-18 01:57:45 +0000
3+++ breezy/branch.py 2020-02-21 04:09:29 +0000
4@@ -1188,7 +1188,8 @@
5 if revno < 1 or revno > self.revno():
6 raise errors.InvalidRevisionNumber(revno)
7
8- def clone(self, to_controldir, revision_id=None, repository_policy=None):
9+ def clone(self, to_controldir, revision_id=None, repository_policy=None,
10+ tag_selector=None):
11 """Clone this branch into to_controldir preserving all semantic values.
12
13 Most API users will want 'create_clone_on_transport', which creates a
14@@ -1201,11 +1202,12 @@
15 with self.lock_read(), result.lock_write():
16 if repository_policy is not None:
17 repository_policy.configure_branch(result)
18- self.copy_content_into(result, revision_id=revision_id)
19+ self.copy_content_into(
20+ result, revision_id=revision_id, tag_selector=tag_selector)
21 return result
22
23 def sprout(self, to_controldir, revision_id=None, repository_policy=None,
24- repository=None, lossy=False):
25+ repository=None, lossy=False, tag_selector=None):
26 """Create a new line of development from the branch, into to_controldir.
27
28 to_controldir controls the branch format.
29@@ -1222,7 +1224,8 @@
30 with self.lock_read(), result.lock_write():
31 if repository_policy is not None:
32 repository_policy.configure_branch(result)
33- self.copy_content_into(result, revision_id=revision_id)
34+ self.copy_content_into(
35+ result, revision_id=revision_id, tag_selector=tag_selector)
36 master_url = self.get_bound_location()
37 if master_url is None:
38 result.set_parent(self.user_url)
39@@ -1255,14 +1258,16 @@
40 revno = 1
41 destination.set_last_revision_info(revno, revision_id)
42
43- def copy_content_into(self, destination, revision_id=None):
44+ def copy_content_into(self, destination, revision_id=None, tag_selector=None):
45 """Copy the content of self into destination.
46
47 revision_id: if not None, the revision history in the new branch will
48 be truncated to end with revision_id.
49+ tag_selector: Optional callback that receives a tag name
50+ and should return a boolean to indicate whether a tag should be copied
51 """
52 return InterBranch.get(self, destination).copy_content_into(
53- revision_id=revision_id)
54+ revision_id=revision_id, tag_selector=tag_selector)
55
56 def update_references(self, target):
57 if not self._format.supports_reference_locations:
58@@ -1307,7 +1312,8 @@
59
60 def create_clone_on_transport(self, to_transport, revision_id=None,
61 stacked_on=None, create_prefix=False,
62- use_existing_dir=False, no_tree=None):
63+ use_existing_dir=False, no_tree=None,
64+ tag_selector=None):
65 """Create a clone of this branch and its bzrdir.
66
67 :param to_transport: The transport to clone onto.
68@@ -1327,7 +1333,7 @@
69 dir_to = self.controldir.clone_on_transport(
70 to_transport, revision_id=revision_id, stacked_on=stacked_on,
71 create_prefix=create_prefix, use_existing_dir=use_existing_dir,
72- no_tree=no_tree)
73+ no_tree=no_tree, tag_selector=tag_selector)
74 return dir_to.open_branch()
75
76 def create_checkout(self, to_location, revision_id=None,
77@@ -2054,7 +2060,7 @@
78 raise NotImplementedError(klass._get_branch_formats_to_test)
79
80 def pull(self, overwrite=False, stop_revision=None,
81- possible_transports=None, local=False):
82+ possible_transports=None, local=False, tag_selector=None):
83 """Mirror source into target branch.
84
85 The target branch is considered to be 'local', having low latency.
86@@ -2064,18 +2070,21 @@
87 raise NotImplementedError(self.pull)
88
89 def push(self, overwrite=False, stop_revision=None, lossy=False,
90- _override_hook_source_branch=None):
91+ _override_hook_source_branch=None, tag_selector=None):
92 """Mirror the source branch into the target branch.
93
94 The source branch is considered to be 'local', having low latency.
95 """
96 raise NotImplementedError(self.push)
97
98- def copy_content_into(self, revision_id=None):
99+ def copy_content_into(self, revision_id=None, tag_selector=None):
100 """Copy the content of source into target
101
102- revision_id: if not None, the revision history in the new branch will
103- be truncated to end with revision_id.
104+ :param revision_id:
105+ if not None, the revision history in the new branch will
106+ be truncated to end with revision_id.
107+ :param tag_selector: Optional callback that can decide
108+ to copy or not copy tags.
109 """
110 raise NotImplementedError(self.copy_content_into)
111
112@@ -2122,7 +2131,7 @@
113 return format._custom_format
114 return format
115
116- def copy_content_into(self, revision_id=None):
117+ def copy_content_into(self, revision_id=None, tag_selector=None):
118 """Copy the content of source into target
119
120 revision_id: if not None, the revision history in the new branch will
121@@ -2139,7 +2148,7 @@
122 if parent:
123 self.target.set_parent(parent)
124 if self.source._push_should_merge_tags():
125- self.source.tags.merge_to(self.target.tags)
126+ self.source.tags.merge_to(self.target.tags, selector=tag_selector)
127
128 def fetch(self, stop_revision=None, limit=None, lossy=False):
129 if self.target.base == self.source.base:
130@@ -2200,7 +2209,8 @@
131
132 def pull(self, overwrite=False, stop_revision=None,
133 possible_transports=None, run_hooks=True,
134- _override_hook_target=None, local=False):
135+ _override_hook_target=None, local=False,
136+ tag_selector=None):
137 """Pull from source into self, updating my master if any.
138
139 :param run_hooks: Private parameter - if false, this branch
140@@ -2231,15 +2241,17 @@
141 if master_branch:
142 # pull from source into master.
143 master_branch.pull(
144- self.source, overwrite, stop_revision, run_hooks=False)
145+ self.source, overwrite, stop_revision, run_hooks=False,
146+ tag_selector=tag_selector)
147 return self._pull(
148 overwrite, stop_revision, _hook_master=master_branch,
149 run_hooks=run_hooks,
150 _override_hook_target=_override_hook_target,
151- merge_tags_to_master=not source_is_master)
152+ merge_tags_to_master=not source_is_master,
153+ tag_selector=tag_selector)
154
155 def push(self, overwrite=False, stop_revision=None, lossy=False,
156- _override_hook_source_branch=None):
157+ _override_hook_source_branch=None, tag_selector=None):
158 """See InterBranch.push.
159
160 This is the basic concrete implementation of push()
161@@ -2271,18 +2283,21 @@
162 with master_branch.lock_write():
163 # push into the master from the source branch.
164 master_inter = InterBranch.get(self.source, master_branch)
165- master_inter._basic_push(overwrite, stop_revision)
166+ master_inter._basic_push(
167+ overwrite, stop_revision, tag_selector=tag_selector)
168 # and push into the target branch from the source. Note
169 # that we push from the source branch again, because it's
170 # considered the highest bandwidth repository.
171- result = self._basic_push(overwrite, stop_revision)
172+ result = self._basic_push(
173+ overwrite, stop_revision, tag_selector=tag_selector)
174 result.master_branch = master_branch
175 result.local_branch = self.target
176 _run_hooks()
177 else:
178 master_branch = None
179 # no master branch
180- result = self._basic_push(overwrite, stop_revision)
181+ result = self._basic_push(
182+ overwrite, stop_revision, tag_selector=tag_selector)
183 # TODO: Why set master_branch and local_branch if there's no
184 # binding? Maybe cleaner to just leave them unset? -- mbp
185 # 20070504
186@@ -2291,7 +2306,7 @@
187 _run_hooks()
188 return result
189
190- def _basic_push(self, overwrite, stop_revision):
191+ def _basic_push(self, overwrite, stop_revision, tag_selector=None):
192 """Basic implementation of push without bound branches or hooks.
193
194 Must be called with source read locked and target write locked.
195@@ -2310,7 +2325,7 @@
196 if self.source._push_should_merge_tags():
197 result.tag_updates, result.tag_conflicts = (
198 self.source.tags.merge_to(
199- self.target.tags, "tags" in overwrite))
200+ self.target.tags, "tags" in overwrite, selector=tag_selector))
201 self.update_references()
202 result.new_revno, result.new_revid = self.target.last_revision_info()
203 return result
204@@ -2318,7 +2333,7 @@
205 def _pull(self, overwrite=False, stop_revision=None,
206 possible_transports=None, _hook_master=None, run_hooks=True,
207 _override_hook_target=None, local=False,
208- merge_tags_to_master=True):
209+ merge_tags_to_master=True, tag_selector=None):
210 """See Branch.pull.
211
212 This function is the core worker, used by GenericInterBranch.pull to
213@@ -2362,7 +2377,8 @@
214 result.tag_updates, result.tag_conflicts = (
215 self.source.tags.merge_to(
216 self.target.tags, "tags" in overwrite,
217- ignore_master=not merge_tags_to_master))
218+ ignore_master=not merge_tags_to_master,
219+ selector=tag_selector))
220 self.update_references()
221 result.new_revno, result.new_revid = (
222 self.target.last_revision_info())
223
224=== modified file 'breezy/bzr/branch.py'
225--- breezy/bzr/branch.py 2020-02-18 01:57:45 +0000
226+++ breezy/bzr/branch.py 2020-02-21 04:09:29 +0000
227@@ -27,6 +27,8 @@
228 lockdir,
229 rio,
230 shelf,
231+ )
232+from breezy.bzr import (
233 tag as _mod_tag,
234 )
235 """)
236@@ -1022,7 +1024,7 @@
237 def _make_reference_clone_function(format, a_branch):
238 """Create a clone() routine for a branch dynamically."""
239 def clone(to_bzrdir, revision_id=None,
240- repository_policy=None):
241+ repository_policy=None, tag_selector=None):
242 """See Branch.clone()."""
243 return format.initialize(to_bzrdir, target_branch=a_branch)
244 # cannot obey revision_id limits when cloning a reference ...
245
246=== modified file 'breezy/bzr/bzrdir.py'
247--- breezy/bzr/bzrdir.py 2020-02-18 01:57:45 +0000
248+++ breezy/bzr/bzrdir.py 2020-02-21 04:09:29 +0000
249@@ -141,7 +141,8 @@
250
251 def clone_on_transport(self, transport, revision_id=None,
252 force_new_repo=False, preserve_stacking=False, stacked_on=None,
253- create_prefix=False, use_existing_dir=True, no_tree=False):
254+ create_prefix=False, use_existing_dir=True, no_tree=False,
255+ tag_selector=None):
256 """Clone this bzrdir and its contents to transport verbatim.
257
258 :param transport: The transport for the location to produce the clone
259@@ -233,7 +234,8 @@
260 if local_branch is not None:
261 local_branch.clone(
262 result, revision_id=revision_id,
263- repository_policy=repository_policy)
264+ repository_policy=repository_policy,
265+ tag_selector=tag_selector)
266 try:
267 # Cheaper to check if the target is not local, than to try making
268 # the tree and fail.
269
270=== modified file 'breezy/bzr/knitpack_repo.py'
271--- breezy/bzr/knitpack_repo.py 2020-02-18 01:57:45 +0000
272+++ breezy/bzr/knitpack_repo.py 2020-02-21 04:09:29 +0000
273@@ -264,8 +264,6 @@
274 class RepositoryFormatKnitPack5(RepositoryFormatPack):
275 """Repository that supports external references to allow stacking.
276
277- New in release 1.6.
278-
279 Supports external lookups, which results in non-truncated ghosts after
280 reconcile compared to pack-0.92 formats.
281 """
282@@ -303,8 +301,6 @@
283 class RepositoryFormatKnitPack5RichRoot(RepositoryFormatPack):
284 """A repository with rich roots and stacking.
285
286- New in release 1.6.1.
287-
288 Supports stacking on other repositories, allowing data to be accessed
289 without being stored locally.
290 """
291@@ -344,8 +340,6 @@
292 class RepositoryFormatKnitPack5RichRootBroken(RepositoryFormatPack):
293 """A repository with rich roots and external references.
294
295- New in release 1.6.
296-
297 Supports external lookups, which results in non-truncated ghosts after
298 reconcile compared to pack-0.92 formats.
299
300
301=== modified file 'breezy/bzr/remote.py'
302--- breezy/bzr/remote.py 2020-02-18 01:57:45 +0000
303+++ breezy/bzr/remote.py 2020-02-21 04:09:29 +0000
304@@ -4019,12 +4019,12 @@
305 source, overwrite=overwrite, stop_revision=stop_revision,
306 _override_hook_target=self, **kwargs)
307
308- def push(self, target, overwrite=False, stop_revision=None, lossy=False):
309+ def push(self, target, overwrite=False, stop_revision=None, lossy=False, tag_selector=None):
310 with self.lock_read():
311 self._ensure_real()
312 return self._real_branch.push(
313 target, overwrite=overwrite, stop_revision=stop_revision, lossy=lossy,
314- _override_hook_source_branch=self)
315+ _override_hook_source_branch=self, tag_selector=tag_selector)
316
317 def peek_lock_mode(self):
318 return self._lock_mode
319
320=== added file 'breezy/bzr/tag.py'
321--- breezy/bzr/tag.py 1970-01-01 00:00:00 +0000
322+++ breezy/bzr/tag.py 2020-02-21 04:09:29 +0000
323@@ -0,0 +1,113 @@
324+# Copyright (C) 2005-2012 Canonical Ltd
325+# Copyright (C) 2020 Breezy Developers
326+#
327+# This program is free software; you can redistribute it and/or modify
328+# it under the terms of the GNU General Public License as published by
329+# the Free Software Foundation; either version 2 of the License, or
330+# (at your option) any later version.
331+#
332+# This program is distributed in the hope that it will be useful,
333+# but WITHOUT ANY WARRANTY; without even the implied warranty of
334+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
335+# GNU General Public License for more details.
336+#
337+# You should have received a copy of the GNU General Public License
338+# along with this program; if not, write to the Free Software
339+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
340+
341+from __future__ import absolute_import
342+
343+from ..tag import Tags
344+
345+from .. import (
346+ bencode,
347+ errors,
348+ trace,
349+ )
350+
351+
352+class BasicTags(Tags):
353+ """Tag storage in an unversioned branch control file.
354+ """
355+
356+ def set_tag(self, tag_name, tag_target):
357+ """Add a tag definition to the branch.
358+
359+ Behaviour if the tag is already present is not defined (yet).
360+ """
361+ # all done with a write lock held, so this looks atomic
362+ with self.branch.lock_write():
363+ master = self.branch.get_master_branch()
364+ if master is not None:
365+ master.tags.set_tag(tag_name, tag_target)
366+ td = self.get_tag_dict()
367+ td[tag_name] = tag_target
368+ self._set_tag_dict(td)
369+
370+ def lookup_tag(self, tag_name):
371+ """Return the referent string of a tag"""
372+ td = self.get_tag_dict()
373+ try:
374+ return td[tag_name]
375+ except KeyError:
376+ raise errors.NoSuchTag(tag_name)
377+
378+ def get_tag_dict(self):
379+ with self.branch.lock_read():
380+ try:
381+ tag_content = self.branch._get_tags_bytes()
382+ except errors.NoSuchFile:
383+ # ugly, but only abentley should see this :)
384+ trace.warning('No branch/tags file in %s. '
385+ 'This branch was probably created by bzr 0.15pre. '
386+ 'Create an empty file to silence this message.'
387+ % (self.branch, ))
388+ return {}
389+ return self._deserialize_tag_dict(tag_content)
390+
391+ def delete_tag(self, tag_name):
392+ """Delete a tag definition.
393+ """
394+ with self.branch.lock_write():
395+ d = self.get_tag_dict()
396+ try:
397+ del d[tag_name]
398+ except KeyError:
399+ raise errors.NoSuchTag(tag_name)
400+ master = self.branch.get_master_branch()
401+ if master is not None:
402+ try:
403+ master.tags.delete_tag(tag_name)
404+ except errors.NoSuchTag:
405+ pass
406+ self._set_tag_dict(d)
407+
408+ def _set_tag_dict(self, new_dict):
409+ """Replace all tag definitions
410+
411+ WARNING: Calling this on an unlocked branch will lock it, and will
412+ replace the tags without warning on conflicts.
413+
414+ :param new_dict: Dictionary from tag name to target.
415+ """
416+ return self.branch._set_tags_bytes(self._serialize_tag_dict(new_dict))
417+
418+ def _serialize_tag_dict(self, tag_dict):
419+ td = dict((k.encode('utf-8'), v)
420+ for k, v in tag_dict.items())
421+ return bencode.bencode(td)
422+
423+ def _deserialize_tag_dict(self, tag_content):
424+ """Convert the tag file into a dictionary of tags"""
425+ # was a special case to make initialization easy, an empty definition
426+ # is an empty dictionary
427+ if tag_content == b'':
428+ return {}
429+ try:
430+ r = {}
431+ for k, v in bencode.bdecode(tag_content).items():
432+ r[k.decode('utf-8')] = v
433+ return r
434+ except ValueError as e:
435+ raise ValueError("failed to deserialize tag dictionary %r: %s"
436+ % (tag_content, e))
437
438=== modified file 'breezy/controldir.py'
439--- breezy/controldir.py 2020-02-18 01:57:45 +0000
440+++ breezy/controldir.py 2020-02-21 04:09:29 +0000
441@@ -398,7 +398,8 @@
442 raise NotImplementedError(self.sprout)
443
444 def push_branch(self, source, revision_id=None, overwrite=False,
445- remember=False, create_prefix=False, lossy=False):
446+ remember=False, create_prefix=False, lossy=False,
447+ tag_selector=None):
448 """Push the source branch into this ControlDir."""
449 br_to = None
450 # If we can open a branch, use its direct repository, otherwise see
451@@ -422,7 +423,9 @@
452 # revision
453 revision_id = source.last_revision()
454 repository_to.fetch(source.repository, revision_id=revision_id)
455- br_to = source.sprout(self, revision_id=revision_id, lossy=lossy)
456+ br_to = source.sprout(
457+ self, revision_id=revision_id, lossy=lossy,
458+ tag_selector=tag_selector)
459 if source.get_push_location() is None or remember:
460 # FIXME: Should be done only if we succeed ? -- vila 2012-01-18
461 source.set_push_location(br_to.base)
462@@ -442,17 +445,19 @@
463 tree_to = self.open_workingtree()
464 except errors.NotLocalUrl:
465 push_result.branch_push_result = source.push(
466- br_to, overwrite, stop_revision=revision_id, lossy=lossy)
467+ br_to, overwrite, stop_revision=revision_id, lossy=lossy,
468+ tag_selector=tag_selector)
469 push_result.workingtree_updated = False
470 except errors.NoWorkingTree:
471 push_result.branch_push_result = source.push(
472- br_to, overwrite, stop_revision=revision_id, lossy=lossy)
473+ br_to, overwrite, stop_revision=revision_id, lossy=lossy,
474+ tag_selector=tag_selector)
475 push_result.workingtree_updated = None # Not applicable
476 else:
477 with tree_to.lock_write():
478 push_result.branch_push_result = source.push(
479 tree_to.branch, overwrite, stop_revision=revision_id,
480- lossy=lossy)
481+ lossy=lossy, tag_selector=tag_selector)
482 tree_to.update()
483 push_result.workingtree_updated = True
484 push_result.old_revno = push_result.branch_push_result.old_revno
485@@ -491,7 +496,7 @@
486 raise NotImplementedError(self.check_conversion_target)
487
488 def clone(self, url, revision_id=None, force_new_repo=False,
489- preserve_stacking=False):
490+ preserve_stacking=False, tag_selector=None):
491 """Clone this controldir and its contents to url verbatim.
492
493 :param url: The url create the clone at. If url's last component does
494@@ -507,11 +512,13 @@
495 return self.clone_on_transport(_mod_transport.get_transport(url),
496 revision_id=revision_id,
497 force_new_repo=force_new_repo,
498- preserve_stacking=preserve_stacking)
499+ preserve_stacking=preserve_stacking,
500+ tag_selector=tag_selector)
501
502 def clone_on_transport(self, transport, revision_id=None,
503 force_new_repo=False, preserve_stacking=False, stacked_on=None,
504- create_prefix=False, use_existing_dir=True, no_tree=False):
505+ create_prefix=False, use_existing_dir=True, no_tree=False,
506+ tag_selector=None):
507 """Clone this controldir and its contents to transport verbatim.
508
509 :param transport: The transport for the location to produce the clone
510
511=== modified file 'breezy/git/branch.py'
512--- breezy/git/branch.py 2020-02-18 01:57:45 +0000
513+++ breezy/git/branch.py 2020-02-21 04:09:29 +0000
514@@ -41,7 +41,6 @@
515 lock,
516 repository as _mod_repository,
517 revision,
518- tag,
519 trace,
520 transport,
521 urlutils,
522@@ -50,6 +49,10 @@
523 from ..revision import (
524 NULL_REVISION,
525 )
526+from ..tag import (
527+ Tags,
528+ InterTags,
529+ )
530 from ..trace import (
531 is_quiet,
532 mutter,
533@@ -111,44 +114,75 @@
534 return self._lookup_revno(self.new_revid)
535
536
537-class GitTags(tag.BasicTags):
538- """Ref-based tag dictionary."""
539-
540- def __init__(self, branch):
541- self.branch = branch
542- self.repository = branch.repository
543-
544- def _merge_to_remote_git(self, target_repo, source_tag_refs,
545- overwrite=False):
546+class InterTagsFromGitToRemoteGit(InterTags):
547+
548+ @classmethod
549+ def is_compatible(klass, source, target):
550+ if not isinstance(source, GitTags):
551+ return False
552+ if not isinstance(target, GitTags):
553+ return False
554+ if getattr(target.branch.repository, "_git", None) is not None:
555+ return False
556+ return True
557+
558+ def merge(self, overwrite=False, ignore_master=False, selector=None):
559+ if self.source.branch.repository.has_same_location(self.target.branch.repository):
560+ return {}, []
561 updates = {}
562 conflicts = []
563+ source_tag_refs = self.source.branch.get_tag_refs()
564
565 def get_changed_refs(old_refs):
566 ret = dict(old_refs)
567 for ref_name, tag_name, peeled, unpeeled in (
568 source_tag_refs.iteritems()):
569+ if selector and not selector(tag_name):
570+ continue
571 if old_refs.get(ref_name) == unpeeled:
572 pass
573 elif overwrite or ref_name not in old_refs:
574 ret[ref_name] = unpeeled
575- updates[tag_name] = target_repo.lookup_foreign_revision_id(
576+ updates[tag_name] = self.target.branch.repository.lookup_foreign_revision_id(
577 peeled)
578+ self.target.branch._tag_refs = None
579 else:
580 conflicts.append(
581 (tag_name,
582 self.repository.lookup_foreign_revision_id(peeled),
583- target_repo.lookup_foreign_revision_id(
584+ self.target.branch.repository.lookup_foreign_revision_id(
585 old_refs[ref_name])))
586 return ret
587- target_repo.controldir.send_pack(
588+ self.target.branch.repository.controldir.send_pack(
589 get_changed_refs, lambda have, want: [])
590- return updates, conflicts
591-
592- def _merge_to_local_git(self, target_repo, source_tag_refs,
593- overwrite=False):
594+ return updates, set(conflicts)
595+
596+
597+class InterTagsFromGitToLocalGit(InterTags):
598+
599+ @classmethod
600+ def is_compatible(klass, source, target):
601+ if not isinstance(source, GitTags):
602+ return False
603+ if not isinstance(target, GitTags):
604+ return False
605+ if getattr(target.branch.repository, "_git", None) is None:
606+ return False
607+ return True
608+
609+ def merge(self, overwrite=False, ignore_master=False, selector=None):
610+ if self.source.branch.repository.has_same_location(self.target.branch.repository):
611+ return {}, []
612+
613 conflicts = []
614 updates = {}
615+ source_tag_refs = self.source.branch.get_tag_refs()
616+
617+ target_repo = self.target.branch.repository
618+
619 for ref_name, tag_name, peeled, unpeeled in source_tag_refs:
620+ if selector and not selector(tag_name):
621+ continue
622 if target_repo._git.refs.get(ref_name) == unpeeled:
623 pass
624 elif overwrite or ref_name not in target_repo._git.refs:
625@@ -164,9 +198,10 @@
626 tag_name)
627 continue
628 target_repo._git.refs[ref_name] = unpeeled or peeled
629+ self.target.branch._tag_refs = None
630 else:
631 try:
632- source_revid = self.repository.lookup_foreign_revision_id(
633+ source_revid = self.source.branch.repository.lookup_foreign_revision_id(
634 peeled)
635 target_revid = target_repo.lookup_foreign_revision_id(
636 target_repo._git.refs[ref_name])
637@@ -179,32 +214,54 @@
638 tag_name)
639 continue
640 conflicts.append((tag_name, source_revid, target_revid))
641- return updates, conflicts
642-
643- def _merge_to_git(self, to_tags, source_tag_refs, overwrite=False):
644- target_repo = to_tags.repository
645- if self.repository.has_same_location(target_repo):
646- return {}, []
647- try:
648- if getattr(target_repo, "_git", None):
649- return self._merge_to_local_git(
650- target_repo, source_tag_refs, overwrite)
651- else:
652- return self._merge_to_remote_git(
653- target_repo, source_tag_refs, overwrite)
654- finally:
655- to_tags.branch._tag_refs = None
656-
657- def _merge_to_non_git(self, to_tags, source_tag_refs, overwrite=False):
658+ return updates, set(conflicts)
659+
660+
661+class InterTagsFromGitToNonGit(InterTags):
662+
663+ @classmethod
664+ def is_compatible(klass, source, target):
665+ if not isinstance(source, GitTags):
666+ return False
667+ if isinstance(target, GitTags):
668+ return False
669+ return True
670+
671+ def merge(self, overwrite=False, ignore_master=False, selector=None):
672+ """See Tags.merge_to."""
673+ source_tag_refs = self.source.branch.get_tag_refs()
674+ if ignore_master:
675+ master = None
676+ else:
677+ master = self.target.branch.get_master_branch()
678+ with contextlib.ExitStack() as es:
679+ if master is not None:
680+ es.enter_context(master.lock_write())
681+ updates, conflicts = self._merge_to(
682+ self.target, source_tag_refs, overwrite=overwrite,
683+ selector=selector)
684+ if master is not None:
685+ extra_updates, extra_conflicts = self._merge_to(
686+ master.tags, overwrite=overwrite,
687+ source_tag_refs=source_tag_refs,
688+ ignore_master=ignore_master, selector=selector)
689+ updates.update(extra_updates)
690+ conflicts.update(extra_conflicts)
691+ return updates, conflicts
692+
693+ def _merge_to(self, to_tags, source_tag_refs, overwrite=False,
694+ selector=None):
695 unpeeled_map = defaultdict(set)
696 conflicts = []
697 updates = {}
698 result = dict(to_tags.get_tag_dict())
699 for ref_name, tag_name, peeled, unpeeled in source_tag_refs:
700+ if selector and not selector(tag_name):
701+ continue
702 if unpeeled is not None:
703 unpeeled_map[peeled].add(unpeeled)
704 try:
705- bzr_revid = self.branch.lookup_foreign_revision_id(peeled)
706+ bzr_revid = self.source.branch.lookup_foreign_revision_id(peeled)
707 except NotCommitError:
708 continue
709 if result.get(tag_name) == bzr_revid:
710@@ -219,36 +276,20 @@
711 map_file = UnpeelMap.from_repository(to_tags.branch.repository)
712 map_file.update(unpeeled_map)
713 map_file.save_in_repository(to_tags.branch.repository)
714- return updates, conflicts
715-
716- def merge_to(self, to_tags, overwrite=False, ignore_master=False,
717- source_tag_refs=None):
718- """See Tags.merge_to."""
719- if source_tag_refs is None:
720- source_tag_refs = self.branch.get_tag_refs()
721- if self == to_tags:
722- return {}, []
723- if isinstance(to_tags, GitTags):
724- return self._merge_to_git(to_tags, source_tag_refs,
725- overwrite=overwrite)
726- else:
727- if ignore_master:
728- master = None
729- else:
730- master = to_tags.branch.get_master_branch()
731- with contextlib.ExitStack() as es:
732- if master is not None:
733- es.enter_context(master.lock_write())
734- updates, conflicts = self._merge_to_non_git(
735- to_tags, source_tag_refs, overwrite=overwrite)
736- if master is not None:
737- extra_updates, extra_conflicts = self.merge_to(
738- master.tags, overwrite=overwrite,
739- source_tag_refs=source_tag_refs,
740- ignore_master=ignore_master)
741- updates.update(extra_updates)
742- conflicts += extra_conflicts
743- return updates, conflicts
744+ return updates, set(conflicts)
745+
746+
747+InterTags.register_optimiser(InterTagsFromGitToRemoteGit)
748+InterTags.register_optimiser(InterTagsFromGitToLocalGit)
749+InterTags.register_optimiser(InterTagsFromGitToNonGit)
750+
751+
752+class GitTags(Tags):
753+ """Ref-based tag dictionary."""
754+
755+ def __init__(self, branch):
756+ self.branch = branch
757+ self.repository = branch.repository
758
759 def get_tag_dict(self):
760 ret = {}
761@@ -262,6 +303,15 @@
762 ret[tag_name] = bzr_revid
763 return ret
764
765+ def lookup_tag(self, tag_name):
766+ """Return the referent string of a tag"""
767+ # TODO(jelmer): Replace with something more efficient for local tags.
768+ td = self.get_tag_dict()
769+ try:
770+ return td[tag_name]
771+ except KeyError:
772+ raise errors.NoSuchTag(tag_name)
773+
774
775 class LocalGitTagDict(GitTags):
776 """Dictionary with tags in a local repository."""
777@@ -612,9 +662,10 @@
778 return revision.NULL_REVISION
779 return self.lookup_foreign_revision_id(self.head)
780
781- def _basic_push(self, target, overwrite=False, stop_revision=None):
782+ def _basic_push(self, target, overwrite=False, stop_revision=None,
783+ tag_selector=None):
784 return branch.InterBranch.get(self, target)._basic_push(
785- overwrite, stop_revision)
786+ overwrite, stop_revision, tag_selector=tag_selector)
787
788 def lookup_foreign_revision_id(self, foreign_revid):
789 try:
790@@ -934,7 +985,7 @@
791 stop_revision, fetch_tags=fetch_tags, limit=limit, lossy=lossy)
792 return _mod_repository.FetchResult()
793
794- def fetch_objects(self, stop_revision, fetch_tags, limit=None, lossy=False):
795+ def fetch_objects(self, stop_revision, fetch_tags, limit=None, lossy=False, tag_selector=None):
796 interrepo = self._get_interrepo(self.source, self.target)
797 if fetch_tags is None:
798 c = self.source.get_config_stack()
799@@ -952,7 +1003,7 @@
800 else:
801 self._last_revid = stop_revision
802 real = interrepo.get_determine_wants_revids(
803- [self._last_revid], include_tags=fetch_tags)
804+ [self._last_revid], include_tags=fetch_tags, tag_selector=tag_selector)
805 return real(heads)
806 pack_hint, head, refs = interrepo.fetch_objects(
807 determine_wants, self.source.mapping, limit=limit,
808@@ -962,8 +1013,8 @@
809 self.target.repository.pack(hint=pack_hint)
810 return head, refs
811
812- def _update_revisions(self, stop_revision=None, overwrite=False):
813- head, refs = self.fetch_objects(stop_revision, fetch_tags=None)
814+ def _update_revisions(self, stop_revision=None, overwrite=False, tag_selector=None):
815+ head, refs = self.fetch_objects(stop_revision, fetch_tags=None, tag_selector=tag_selector)
816 if overwrite:
817 prev_last_revid = None
818 else:
819@@ -988,7 +1039,7 @@
820 pass
821
822 def _basic_pull(self, stop_revision, overwrite, run_hooks,
823- _override_hook_target, _hook_master):
824+ _override_hook_target, _hook_master, tag_selector=None):
825 if overwrite is True:
826 overwrite = set(["history", "tags"])
827 elif not overwrite:
828@@ -1005,7 +1056,8 @@
829 (result.old_revno, result.old_revid) = \
830 self.target.last_revision_info()
831 result.new_git_head, remote_refs = self._update_revisions(
832- stop_revision, overwrite=("history" in overwrite))
833+ stop_revision, overwrite=("history" in overwrite),
834+ tag_selector=tag_selector)
835 tags_ret = self.source.tags.merge_to(
836 self.target.tags, ("tags" in overwrite), ignore_master=True)
837 if isinstance(tags_ret, tuple):
838@@ -1028,7 +1080,7 @@
839
840 def pull(self, overwrite=False, stop_revision=None,
841 possible_transports=None, _hook_master=None, run_hooks=True,
842- _override_hook_target=None, local=False):
843+ _override_hook_target=None, local=False, tag_selector=None):
844 """See Branch.pull.
845
846 :param _hook_master: Private parameter - set the branch to
847@@ -1066,9 +1118,10 @@
848 master_branch = None
849 return self._basic_pull(stop_revision, overwrite, run_hooks,
850 _override_hook_target,
851- _hook_master=master_branch)
852+ _hook_master=master_branch,
853+ tag_selector=tag_selector)
854
855- def _basic_push(self, overwrite, stop_revision):
856+ def _basic_push(self, overwrite, stop_revision, tag_selector=None):
857 if overwrite is True:
858 overwrite = set(["history", "tags"])
859 elif not overwrite:
860@@ -1078,9 +1131,11 @@
861 result.target_branch = self.target
862 result.old_revno, result.old_revid = self.target.last_revision_info()
863 result.new_git_head, remote_refs = self._update_revisions(
864- stop_revision, overwrite=("history" in overwrite))
865+ stop_revision, overwrite=("history" in overwrite),
866+ tag_selector=tag_selector)
867 tags_ret = self.source.tags.merge_to(
868- self.target.tags, "tags" in overwrite, ignore_master=True)
869+ self.target.tags, "tags" in overwrite, ignore_master=True,
870+ selector=tag_selector)
871 (result.tag_updates, result.tag_conflicts) = tags_ret
872 result.new_revno, result.new_revid = self.target.last_revision_info()
873 self.update_references(revid=result.new_revid)
874@@ -1109,7 +1164,7 @@
875 return (isinstance(source, LocalGitBranch) and
876 isinstance(target, RemoteGitBranch))
877
878- def _basic_push(self, overwrite, stop_revision):
879+ def _basic_push(self, overwrite, stop_revision, tag_selector=None):
880 result = GitBranchPushResult()
881 result.source_branch = self.source
882 result.target_branch = self.target
883@@ -1134,6 +1189,8 @@
884 result.new_revid = stop_revision
885 for name, sha in (
886 self.source.repository._git.refs.as_dict(b"refs/tags").items()):
887+ if tag_selector and not tag_selector(name):
888+ continue
889 if sha not in self.source.repository._git:
890 trace.mutter('Ignoring missing SHA: %s', sha)
891 continue
892@@ -1173,7 +1230,7 @@
893 interrepo.fetch_objects(determine_wants, limit=limit, lossy=lossy)
894 return _mod_repository.FetchResult()
895
896- def _basic_push(self, overwrite=False, stop_revision=None):
897+ def _basic_push(self, overwrite=False, stop_revision=None, tag_selector=None):
898 if overwrite is True:
899 overwrite = set(["history", "tags"])
900 elif not overwrite:
901@@ -1189,8 +1246,8 @@
902 other_branch=self.source)
903 tags_ret = self.source.tags.merge_to(
904 self.target.tags,
905- source_tag_refs=remote_refs_dict_to_tag_refs(refs),
906- overwrite=("tags" in overwrite))
907+ overwrite=("tags" in overwrite),
908+ selector=tag_selector)
909 if isinstance(tags_ret, tuple):
910 (result.tag_updates, result.tag_conflicts) = tags_ret
911 else:
912@@ -1218,7 +1275,8 @@
913 return result.refs, stop_revision
914
915 def pull(self, stop_revision=None, overwrite=False,
916- possible_transports=None, run_hooks=True, local=False):
917+ possible_transports=None, run_hooks=True, local=False,
918+ tag_selector=None):
919 # This type of branch can't be bound.
920 if local:
921 raise errors.LocalRequiresBoundBranch()
922@@ -1239,7 +1297,7 @@
923 other_branch=self.source)
924 tags_ret = self.source.tags.merge_to(
925 self.target.tags, overwrite=("tags" in overwrite),
926- source_tag_refs=remote_refs_dict_to_tag_refs(refs))
927+ selector=tag_selector)
928 if isinstance(tags_ret, tuple):
929 (result.tag_updates, result.tag_conflicts) = tags_ret
930 else:
931@@ -1307,7 +1365,7 @@
932 refs[ref] = (None, revid)
933 return refs, main_ref, (stop_revno, stop_revision)
934
935- def _update_refs(self, result, old_refs, new_refs, overwrite):
936+ def _update_refs(self, result, old_refs, new_refs, overwrite, tag_selector):
937 mutter("updating refs. old refs: %r, new refs: %r",
938 old_refs, new_refs)
939 result.tag_updates = {}
940@@ -1346,6 +1404,8 @@
941 except ValueError:
942 pass
943 else:
944+ if tag_selector and not tag_selector(tag_name):
945+ continue
946 result.tag_updates[tag_name] = revid
947 ret[ref] = (git_sha, revid)
948 else:
949@@ -1381,7 +1441,8 @@
950 for (old_revid, (new_sha, new_revid)) in revidmap.items()})
951
952 def pull(self, overwrite=False, stop_revision=None, local=False,
953- possible_transports=None, run_hooks=True, _stop_revno=None):
954+ possible_transports=None, run_hooks=True, _stop_revno=None,
955+ tag_selector=None):
956 result = GitBranchPullResult()
957 result.source_branch = self.source
958 result.target_branch = self.target
959@@ -1390,7 +1451,7 @@
960 stop_revision, stop_revno=_stop_revno)
961
962 def update_refs(old_refs):
963- return self._update_refs(result, old_refs, new_refs, overwrite)
964+ return self._update_refs(result, old_refs, new_refs, overwrite, tag_selector)
965 try:
966 result.revidmap, old_refs, new_refs = (
967 self.interrepo.fetch_refs(update_refs, lossy=False))
968@@ -1410,7 +1471,8 @@
969 return result
970
971 def push(self, overwrite=False, stop_revision=None, lossy=False,
972- _override_hook_source_branch=None, _stop_revno=None):
973+ _override_hook_source_branch=None, _stop_revno=None,
974+ tag_selector=None):
975 result = GitBranchPushResult()
976 result.source_branch = self.source
977 result.target_branch = self.target
978@@ -1421,7 +1483,7 @@
979 stop_revision, stop_revno=_stop_revno)
980
981 def update_refs(old_refs):
982- return self._update_refs(result, old_refs, new_refs, overwrite)
983+ return self._update_refs(result, old_refs, new_refs, overwrite, tag_selector)
984 try:
985 result.revidmap, old_refs, new_refs = (
986 self.interrepo.fetch_refs(
987
988=== modified file 'breezy/git/dir.py'
989--- breezy/git/dir.py 2020-02-18 01:57:45 +0000
990+++ breezy/git/dir.py 2020-02-21 04:09:29 +0000
991@@ -218,7 +218,8 @@
992 def clone_on_transport(self, transport, revision_id=None,
993 force_new_repo=False, preserve_stacking=False,
994 stacked_on=None, create_prefix=False,
995- use_existing_dir=True, no_tree=False):
996+ use_existing_dir=True, no_tree=False,
997+ tag_selector=None):
998 """See ControlDir.clone_on_transport."""
999 from ..repository import InterRepository
1000 from .mapping import default_mapping
1001@@ -240,7 +241,7 @@
1002 interrepo = InterRepository.get(source_repo, target_repo)
1003 if revision_id is not None:
1004 determine_wants = interrepo.get_determine_wants_revids(
1005- [revision_id], include_tags=True)
1006+ [revision_id], include_tags=True, tag_selector=tag_selector)
1007 else:
1008 determine_wants = interrepo.determine_wants_all
1009 (pack_hint, _, refs) = interrepo.fetch_objects(determine_wants,
1010@@ -312,7 +313,7 @@
1011
1012 def push_branch(self, source, revision_id=None, overwrite=False,
1013 remember=False, create_prefix=False, lossy=False,
1014- name=None):
1015+ name=None, tag_selector=None):
1016 """Push the source branch into this ControlDir."""
1017 push_result = GitPushResult()
1018 push_result.workingtree_updated = None
1019@@ -325,7 +326,7 @@
1020 target = self.open_branch(name, nascent_ok=True)
1021 push_result.branch_push_result = source.push(
1022 target, overwrite=overwrite, stop_revision=revision_id,
1023- lossy=lossy)
1024+ lossy=lossy, tag_selector=tag_selector)
1025 push_result.new_revid = push_result.branch_push_result.new_revid
1026 push_result.old_revid = push_result.branch_push_result.old_revid
1027 try:
1028
1029=== modified file 'breezy/git/interrepo.py'
1030--- breezy/git/interrepo.py 2020-02-18 01:57:45 +0000
1031+++ breezy/git/interrepo.py 2020-02-21 04:09:29 +0000
1032@@ -75,6 +75,7 @@
1033 )
1034 from .refs import (
1035 is_tag,
1036+ ref_to_tag_name,
1037 )
1038 from .repository import (
1039 GitRepository,
1040@@ -403,7 +404,7 @@
1041 def _target_has_shas(self, shas):
1042 raise NotImplementedError(self._target_has_shas)
1043
1044- def get_determine_wants_heads(self, wants, include_tags=False):
1045+ def get_determine_wants_heads(self, wants, include_tags=False, tag_selector=None):
1046 wants = set(wants)
1047
1048 def determine_wants(refs):
1049@@ -416,7 +417,11 @@
1050 for k, sha in refs.items():
1051 if k.endswith(ANNOTATED_TAG_SUFFIX):
1052 continue
1053- if not is_tag(k):
1054+ try:
1055+ tag_name = ref_to_tag_name(k)
1056+ except ValueError:
1057+ continue
1058+ if tag_selector and not tag_selector(tag_name):
1059 continue
1060 if sha == ZERO_SHA:
1061 continue
1062@@ -502,14 +507,15 @@
1063 """
1064 raise NotImplementedError(self.fetch_objects)
1065
1066- def get_determine_wants_revids(self, revids, include_tags=False):
1067+ def get_determine_wants_revids(self, revids, include_tags=False, tag_selector=None):
1068 wants = set()
1069 for revid in set(revids):
1070 if self.target.has_revision(revid):
1071 continue
1072 git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
1073 wants.add(git_sha)
1074- return self.get_determine_wants_heads(wants, include_tags=include_tags)
1075+ return self.get_determine_wants_heads(
1076+ wants, include_tags=include_tags, tag_selector=tag_selector)
1077
1078 def fetch(self, revision_id=None, find_ghosts=False,
1079 mapping=None, fetch_spec=None, include_tags=False, lossy=False):
1080@@ -682,14 +688,14 @@
1081 result.refs = wants_recorder.remote_refs
1082 return result
1083
1084- def get_determine_wants_revids(self, revids, include_tags=False):
1085+ def get_determine_wants_revids(self, revids, include_tags=False, tag_selector=None):
1086 wants = set()
1087 for revid in set(revids):
1088 if revid == NULL_REVISION:
1089 continue
1090 git_sha, mapping = self.source.lookup_bzr_revision_id(revid)
1091 wants.add(git_sha)
1092- return self.get_determine_wants_heads(wants, include_tags=include_tags)
1093+ return self.get_determine_wants_heads(wants, include_tags=include_tags, tag_selector=tag_selector)
1094
1095 def get_determine_wants_branches(self, branches, include_tags=False):
1096 def determine_wants(refs):
1097
1098=== modified file 'breezy/git/workingtree.py'
1099--- breezy/git/workingtree.py 2020-02-18 01:57:45 +0000
1100+++ breezy/git/workingtree.py 2020-02-21 04:09:29 +0000
1101@@ -1210,12 +1210,12 @@
1102
1103 def pull(self, source, overwrite=False, stop_revision=None,
1104 change_reporter=None, possible_transports=None, local=False,
1105- show_base=False):
1106+ show_base=False, tag_selector=None):
1107 with self.lock_write(), source.lock_read():
1108 old_revision = self.branch.last_revision()
1109 count = self.branch.pull(source, overwrite, stop_revision,
1110 possible_transports=possible_transports,
1111- local=local)
1112+ local=local, tag_selector=tag_selector)
1113 self._update_git_tree(
1114 old_revision=old_revision,
1115 new_revision=self.branch.last_revision(),
1116
1117=== modified file 'breezy/plugins/propose/github.py'
1118--- breezy/plugins/propose/github.py 2020-02-18 01:57:45 +0000
1119+++ breezy/plugins/propose/github.py 2020-02-21 04:09:29 +0000
1120@@ -275,11 +275,12 @@
1121 return json.loads(response.text)
1122 raise InvalidHttpResponse(path, response.text)
1123
1124- def _create_pull(self, path, title, head, base, body=None, labels=None, assignee=None):
1125+ def _create_pull(self, path, title, head, base, body=None, labels=None, assignee=None, draft=False):
1126 data = {
1127 'title': title,
1128 'head': head,
1129 'base': base,
1130+ 'draft': draft,
1131 }
1132 if labels is not None:
1133 data['labels'] = labels
1134@@ -373,7 +374,7 @@
1135
1136 def publish_derived(self, local_branch, base_branch, name, project=None,
1137 owner=None, revision_id=None, overwrite=False,
1138- allow_lossy=True):
1139+ allow_lossy=True, tag_selector=None):
1140 base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
1141 base_repo = self._get_repo(base_owner, base_project)
1142 if owner is None:
1143@@ -393,13 +394,14 @@
1144 try:
1145 push_result = remote_dir.push_branch(
1146 local_branch, revision_id=revision_id, overwrite=overwrite,
1147- name=name)
1148+ name=name, tag_selector=tag_selector)
1149 except errors.NoRoundtrippingSupport:
1150 if not allow_lossy:
1151 raise
1152 push_result = remote_dir.push_branch(
1153 local_branch, revision_id=revision_id,
1154- overwrite=overwrite, name=name, lossy=True)
1155+ overwrite=overwrite, name=name, lossy=True,
1156+ tag_selector=tag_selector)
1157 return push_result.target_branch, github_url_to_bzr_url(
1158 remote_repo['html_url'], name)
1159
1160
1161=== modified file 'breezy/plugins/propose/gitlabs.py'
1162--- breezy/plugins/propose/gitlabs.py 2020-02-18 01:57:45 +0000
1163+++ breezy/plugins/propose/gitlabs.py 2020-02-21 04:09:29 +0000
1164@@ -413,7 +413,7 @@
1165
1166 def publish_derived(self, local_branch, base_branch, name, project=None,
1167 owner=None, revision_id=None, overwrite=False,
1168- allow_lossy=True):
1169+ allow_lossy=True, tag_selector=None):
1170 (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch)
1171 if owner is None:
1172 owner = self._get_logged_in_username()
1173@@ -428,13 +428,13 @@
1174 try:
1175 push_result = remote_dir.push_branch(
1176 local_branch, revision_id=revision_id, overwrite=overwrite,
1177- name=name)
1178+ name=name, tag_selector=tag_selector)
1179 except errors.NoRoundtrippingSupport:
1180 if not allow_lossy:
1181 raise
1182 push_result = remote_dir.push_branch(
1183 local_branch, revision_id=revision_id, overwrite=overwrite,
1184- name=name, lossy=True)
1185+ name=name, lossy=True, tag_selector=tag_selector)
1186 public_url = gitlab_url_to_bzr_url(
1187 target_project['http_url_to_repo'], name)
1188 return push_result.target_branch, public_url
1189
1190=== modified file 'breezy/plugins/propose/launchpad.py'
1191--- breezy/plugins/propose/launchpad.py 2020-02-18 01:57:45 +0000
1192+++ breezy/plugins/propose/launchpad.py 2020-02-21 04:09:29 +0000
1193@@ -258,7 +258,8 @@
1194 return "~%s/%s" % (owner, project)
1195
1196 def _publish_git(self, local_branch, base_path, name, owner, project=None,
1197- revision_id=None, overwrite=False, allow_lossy=True):
1198+ revision_id=None, overwrite=False, allow_lossy=True,
1199+ tag_selector=None):
1200 to_path = self._get_derived_git_path(base_path, owner, project)
1201 to_transport = get_transport("git+ssh://git.launchpad.net/" + to_path)
1202 try:
1203@@ -270,21 +271,23 @@
1204 if dir_to is None:
1205 try:
1206 br_to = local_branch.create_clone_on_transport(
1207- to_transport, revision_id=revision_id, name=name)
1208+ to_transport, revision_id=revision_id, name=name,
1209+ tag_selector=tag_selector)
1210 except errors.NoRoundtrippingSupport:
1211 br_to = local_branch.create_clone_on_transport(
1212 to_transport, revision_id=revision_id, name=name,
1213- lossy=True)
1214+ lossy=True, tag_selector=tag_selector)
1215 else:
1216 try:
1217 dir_to = dir_to.push_branch(
1218- local_branch, revision_id, overwrite=overwrite, name=name)
1219+ local_branch, revision_id, overwrite=overwrite, name=name,
1220+ tag_selector=tag_selector)
1221 except errors.NoRoundtrippingSupport:
1222 if not allow_lossy:
1223 raise
1224 dir_to = dir_to.push_branch(
1225 local_branch, revision_id, overwrite=overwrite, name=name,
1226- lossy=True)
1227+ lossy=True, tag_selector=tag_selector)
1228 br_to = dir_to.target_branch
1229 return br_to, (
1230 "https://git.launchpad.net/%s/+ref/%s" % (to_path, name))
1231@@ -310,7 +313,7 @@
1232
1233 def _publish_bzr(self, local_branch, base_branch, name, owner,
1234 project=None, revision_id=None, overwrite=False,
1235- allow_lossy=True):
1236+ allow_lossy=True, tag_selector=None):
1237 to_path = self._get_derived_bzr_path(base_branch, name, owner, project)
1238 to_transport = get_transport("lp:" + to_path)
1239 try:
1240@@ -321,10 +324,11 @@
1241
1242 if dir_to is None:
1243 br_to = local_branch.create_clone_on_transport(
1244- to_transport, revision_id=revision_id)
1245+ to_transport, revision_id=revision_id, tag_selector=tag_selector)
1246 else:
1247 br_to = dir_to.push_branch(
1248- local_branch, revision_id, overwrite=overwrite).target_branch
1249+ local_branch, revision_id, overwrite=overwrite,
1250+ tag_selector=tag_selector).target_branch
1251 return br_to, ("https://code.launchpad.net/" + to_path)
1252
1253 def _split_url(self, url):
1254@@ -341,7 +345,7 @@
1255
1256 def publish_derived(self, local_branch, base_branch, name, project=None,
1257 owner=None, revision_id=None, overwrite=False,
1258- allow_lossy=True):
1259+ allow_lossy=True, tag_selector=None):
1260 """Publish a branch to the site, derived from base_branch.
1261
1262 :param base_branch: branch to derive the new branch from
1263@@ -360,12 +364,12 @@
1264 return self._publish_bzr(
1265 local_branch, base_branch, name, project=project, owner=owner,
1266 revision_id=revision_id, overwrite=overwrite,
1267- allow_lossy=allow_lossy)
1268+ allow_lossy=allow_lossy, tag_selector=tag_selector)
1269 elif base_vcs == 'git':
1270 return self._publish_git(
1271 local_branch, base_path, name, project=project, owner=owner,
1272 revision_id=revision_id, overwrite=overwrite,
1273- allow_lossy=allow_lossy)
1274+ allow_lossy=allow_lossy, tag_selector=tag_selector)
1275 else:
1276 raise AssertionError('not a valid Launchpad URL')
1277
1278
1279=== modified file 'breezy/plugins/weave_fmt/bzrdir.py'
1280--- breezy/plugins/weave_fmt/bzrdir.py 2020-02-18 01:57:45 +0000
1281+++ breezy/plugins/weave_fmt/bzrdir.py 2020-02-21 04:09:29 +0000
1282@@ -745,7 +745,7 @@
1283 return self._format.__class__()
1284
1285 def clone(self, url, revision_id=None, force_new_repo=False,
1286- preserve_stacking=False):
1287+ preserve_stacking=False, tag_selector=None):
1288 """See ControlDir.clone().
1289
1290 force_new_repo has no effect, since this family of formats always
1291@@ -757,7 +757,7 @@
1292 result = self._format._initialize_for_clone(url)
1293 self.open_repository().clone(result, revision_id=revision_id)
1294 from_branch = self.open_branch()
1295- from_branch.clone(result, revision_id=revision_id)
1296+ from_branch.clone(result, revision_id=revision_id, tag_selector=tag_selector)
1297 try:
1298 tree = self.open_workingtree()
1299 except errors.NotLocalUrl:
1300
1301=== modified file 'breezy/propose.py'
1302--- breezy/propose.py 2020-02-18 01:57:45 +0000
1303+++ breezy/propose.py 2020-02-21 04:09:29 +0000
1304@@ -237,7 +237,7 @@
1305
1306 def publish_derived(self, new_branch, base_branch, name, project=None,
1307 owner=None, revision_id=None, overwrite=False,
1308- allow_lossy=True):
1309+ allow_lossy=True, tag_selector=None):
1310 """Publish a branch to the site, derived from base_branch.
1311
1312 :param base_branch: branch to derive the new branch from
1313
1314=== modified file 'breezy/tag.py'
1315--- breezy/tag.py 2020-02-18 01:57:45 +0000
1316+++ breezy/tag.py 2020-02-21 04:09:29 +0000
1317@@ -24,29 +24,22 @@
1318
1319 from collections import defaultdict
1320 import contextlib
1321+import itertools
1322+import re
1323+import sys
1324
1325 # NOTE: I was going to call this tags.py, but vim seems to think all files
1326 # called tags* are ctags files... mbp 20070220.
1327
1328+from .inter import InterObject
1329 from .registry import Registry
1330-from .lazy_import import lazy_import
1331-lazy_import(globals(), """
1332-import itertools
1333-import re
1334-import sys
1335-
1336-from breezy import (
1337- bencode,
1338- trace,
1339- )
1340-""")
1341
1342 from . import (
1343 errors,
1344 )
1345
1346
1347-def _reconcile_tags(source_dict, dest_dict, overwrite):
1348+def _reconcile_tags(source_dict, dest_dict, overwrite, selector):
1349 """Do a two-way merge of two tag dictionaries.
1350
1351 * only in source => source value
1352@@ -62,6 +55,8 @@
1353 updates = {}
1354 result = dict(dest_dict) # copy
1355 for name, target in source_dict.items():
1356+ if selector and not selector(name):
1357+ continue
1358 if result.get(name) == target:
1359 pass
1360 elif name not in result or overwrite:
1361@@ -72,7 +67,7 @@
1362 return result, updates, conflicts
1363
1364
1365-class _Tags(object):
1366+class Tags(object):
1367
1368 def __init__(self, branch):
1369 self.branch = branch
1370@@ -83,21 +78,39 @@
1371 raise NotImplementedError(self.get_tag_dict)
1372
1373 def get_reverse_tag_dict(self):
1374- """Return a dictionary mapping revision ids to list of tags.
1375- """
1376- raise NotImplementedError(self.get_reverse_tag_dict)
1377-
1378- def merge_to(self, to_tags, overwrite=False, ignore_master=False):
1379- """Merge new tags from this tags container into another.
1380-
1381- :param to_tags: Tags container to merge into
1382- :param overwrite: Whether to overwrite existing, divergent, tags.
1383+ """Returns a dict with revisions as keys
1384+ and a list of tags for that revision as value"""
1385+ d = self.get_tag_dict()
1386+ rev = defaultdict(set)
1387+ for key in d:
1388+ rev[d[key]].add(key)
1389+ return rev
1390+
1391+ def merge_to(self, to_tags, overwrite=False, ignore_master=False, selector=None):
1392+ """Copy tags between repositories if necessary and possible.
1393+
1394+ This method has common command-line behaviour about handling
1395+ error cases.
1396+
1397+ All new definitions are copied across, except that tags that already
1398+ exist keep their existing definitions.
1399+
1400+ :param to_tags: Branch to receive these tags
1401+ :param overwrite: Overwrite conflicting tags in the target branch
1402 :param ignore_master: Do not modify the tags in the target's master
1403 branch (if any). Default is false (so the master will be updated).
1404- New in bzr 2.3.
1405- :return: Tuple with tag updates as dictionary and tag conflicts
1406+
1407+ :returns: Tuple with tag_updates and tag_conflicts.
1408+ tag_updates is a dictionary with new tags, None is used for
1409+ removed tags
1410+ tag_conflicts is a set of tags that conflicted, each of which is
1411+ (tagname, source_target, dest_target), or None if no copying was
1412+ done.
1413 """
1414- raise NotImplementedError(self.merge_to)
1415+ intertags = InterTags.get(self, to_tags)
1416+ return intertags.merge(
1417+ overwrite=overwrite, ignore_master=ignore_master,
1418+ selector=selector)
1419
1420 def set_tag(self, tag_name, revision):
1421 """Set a tag.
1422@@ -127,18 +140,21 @@
1423 raise NotImplementedError(self.delete_tag)
1424
1425 def rename_revisions(self, rename_map):
1426- """Replace revision ids according to a rename map.
1427+ """Rename revisions in this tags dictionary.
1428
1429- :param rename_map: Dictionary mapping old revision ids to
1430- new revision ids.
1431+ :param rename_map: Dictionary mapping old revids to new revids
1432 """
1433- raise NotImplementedError(self.rename_revisions)
1434+ reverse_tags = self.get_reverse_tag_dict()
1435+ for revid, names in reverse_tags.items():
1436+ if revid in rename_map:
1437+ for name in names:
1438+ self.set_tag(name, rename_map[revid])
1439
1440 def has_tag(self, tag_name):
1441 return tag_name in self.get_tag_dict()
1442
1443
1444-class DisabledTags(_Tags):
1445+class DisabledTags(Tags):
1446 """Tag storage that refuses to store anything.
1447
1448 This is used by older formats that can't store tags.
1449@@ -153,7 +169,7 @@
1450 lookup_tag = _not_supported
1451 delete_tag = _not_supported
1452
1453- def merge_to(self, to_tags, overwrite=False, ignore_master=False):
1454+ def merge_to(self, to_tags, overwrite=False, ignore_master=False, selector=None):
1455 # we never have anything to copy
1456 return {}, []
1457
1458@@ -166,102 +182,19 @@
1459 return {}
1460
1461
1462-class BasicTags(_Tags):
1463- """Tag storage in an unversioned branch control file.
1464+class InterTags(InterObject):
1465+ """Operations between sets of tags.
1466 """
1467
1468- def set_tag(self, tag_name, tag_target):
1469- """Add a tag definition to the branch.
1470-
1471- Behaviour if the tag is already present is not defined (yet).
1472- """
1473- # all done with a write lock held, so this looks atomic
1474- with self.branch.lock_write():
1475- master = self.branch.get_master_branch()
1476- if master is not None:
1477- master.tags.set_tag(tag_name, tag_target)
1478- td = self.get_tag_dict()
1479- td[tag_name] = tag_target
1480- self._set_tag_dict(td)
1481-
1482- def lookup_tag(self, tag_name):
1483- """Return the referent string of a tag"""
1484- td = self.get_tag_dict()
1485- try:
1486- return td[tag_name]
1487- except KeyError:
1488- raise errors.NoSuchTag(tag_name)
1489-
1490- def get_tag_dict(self):
1491- with self.branch.lock_read():
1492- try:
1493- tag_content = self.branch._get_tags_bytes()
1494- except errors.NoSuchFile:
1495- # ugly, but only abentley should see this :)
1496- trace.warning('No branch/tags file in %s. '
1497- 'This branch was probably created by bzr 0.15pre. '
1498- 'Create an empty file to silence this message.'
1499- % (self.branch, ))
1500- return {}
1501- return self._deserialize_tag_dict(tag_content)
1502-
1503- def get_reverse_tag_dict(self):
1504- """Returns a dict with revisions as keys
1505- and a list of tags for that revision as value"""
1506- d = self.get_tag_dict()
1507- rev = defaultdict(set)
1508- for key in d:
1509- rev[d[key]].add(key)
1510- return rev
1511-
1512- def delete_tag(self, tag_name):
1513- """Delete a tag definition.
1514- """
1515- with self.branch.lock_write():
1516- d = self.get_tag_dict()
1517- try:
1518- del d[tag_name]
1519- except KeyError:
1520- raise errors.NoSuchTag(tag_name)
1521- master = self.branch.get_master_branch()
1522- if master is not None:
1523- try:
1524- master.tags.delete_tag(tag_name)
1525- except errors.NoSuchTag:
1526- pass
1527- self._set_tag_dict(d)
1528-
1529- def _set_tag_dict(self, new_dict):
1530- """Replace all tag definitions
1531-
1532- WARNING: Calling this on an unlocked branch will lock it, and will
1533- replace the tags without warning on conflicts.
1534-
1535- :param new_dict: Dictionary from tag name to target.
1536- """
1537- return self.branch._set_tags_bytes(self._serialize_tag_dict(new_dict))
1538-
1539- def _serialize_tag_dict(self, tag_dict):
1540- td = dict((k.encode('utf-8'), v)
1541- for k, v in tag_dict.items())
1542- return bencode.bencode(td)
1543-
1544- def _deserialize_tag_dict(self, tag_content):
1545- """Convert the tag file into a dictionary of tags"""
1546- # was a special case to make initialization easy, an empty definition
1547- # is an empty dictionary
1548- if tag_content == b'':
1549- return {}
1550- try:
1551- r = {}
1552- for k, v in bencode.bdecode(tag_content).items():
1553- r[k.decode('utf-8')] = v
1554- return r
1555- except ValueError as e:
1556- raise ValueError("failed to deserialize tag dictionary %r: %s"
1557- % (tag_content, e))
1558-
1559- def merge_to(self, to_tags, overwrite=False, ignore_master=False):
1560+ _optimisers = []
1561+ """The available optimised InterTags types."""
1562+
1563+ @classmethod
1564+ def is_compatible(klass, source, target):
1565+ # This is the default implementation
1566+ return True
1567+
1568+ def merge(self, overwrite=False, ignore_master=False, selector=None):
1569 """Copy tags between repositories if necessary and possible.
1570
1571 This method has common command-line behaviour about handling
1572@@ -274,7 +207,9 @@
1573 :param overwrite: Overwrite conflicting tags in the target branch
1574 :param ignore_master: Do not modify the tags in the target's master
1575 branch (if any). Default is false (so the master will be updated).
1576- New in bzr 2.3.
1577+ :param selector: Callback that determines whether a tag should be
1578+ copied. It should take a tag name and as argument and return a
1579+ boolean.
1580
1581 :returns: Tuple with tag_updates and tag_conflicts.
1582 tag_updates is a dictionary with new tags, None is used for
1583@@ -284,12 +219,12 @@
1584 done.
1585 """
1586 with contextlib.ExitStack() as stack:
1587- if self.branch == to_tags.branch:
1588+ if self.source.branch == self.target.branch:
1589 return {}, []
1590- if not self.branch.supports_tags():
1591+ if not self.source.branch.supports_tags():
1592 # obviously nothing to copy
1593 return {}, []
1594- source_dict = self.get_tag_dict()
1595+ source_dict = self.source.get_tag_dict()
1596 if not source_dict:
1597 # no tags in the source, and we don't want to clobber anything
1598 # that's in the destination
1599@@ -306,44 +241,35 @@
1600 # Ideally we'd improve this API to report the different conflicts
1601 # more clearly to the caller, but we don't want to break plugins
1602 # such as bzr-builddeb that use this API.
1603- stack.enter_context(to_tags.branch.lock_write())
1604+ stack.enter_context(self.target.branch.lock_write())
1605 if ignore_master:
1606 master = None
1607 else:
1608- master = to_tags.branch.get_master_branch()
1609+ master = self.target.branch.get_master_branch()
1610 if master is not None:
1611 stack.enter_context(master.lock_write())
1612- updates, conflicts = self._merge_to(to_tags, source_dict, overwrite)
1613+ updates, conflicts = self._merge_to(
1614+ self.target, source_dict, overwrite, selector=selector)
1615 if master is not None:
1616- extra_updates, extra_conflicts = self._merge_to(master.tags,
1617- source_dict, overwrite)
1618+ extra_updates, extra_conflicts = self._merge_to(
1619+ master.tags, source_dict, overwrite, selector=selector)
1620 updates.update(extra_updates)
1621 conflicts += extra_conflicts
1622 # We use set() to remove any duplicate conflicts from the master
1623 # branch.
1624 return updates, set(conflicts)
1625
1626- def _merge_to(self, to_tags, source_dict, overwrite):
1627+ @classmethod
1628+ def _merge_to(cls, to_tags, source_dict, overwrite, selector):
1629 dest_dict = to_tags.get_tag_dict()
1630 result, updates, conflicts = _reconcile_tags(
1631- source_dict, dest_dict, overwrite)
1632+ source_dict, dest_dict, overwrite, selector)
1633 if result != dest_dict:
1634 to_tags._set_tag_dict(result)
1635 return updates, conflicts
1636
1637- def rename_revisions(self, rename_map):
1638- """Rename revisions in this tags dictionary.
1639-
1640- :param rename_map: Dictionary mapping old revids to new revids
1641- """
1642- reverse_tags = self.get_reverse_tag_dict()
1643- for revid, names in reverse_tags.items():
1644- if revid in rename_map:
1645- for name in names:
1646- self.set_tag(name, rename_map[revid])
1647-
1648-
1649-class MemoryTags(_Tags):
1650+
1651+class MemoryTags(Tags):
1652
1653 def __init__(self, tag_dict):
1654 self._tag_dict = tag_dict
1655@@ -359,15 +285,6 @@
1656 except KeyError:
1657 raise errors.NoSuchTag(tag_name)
1658
1659- def get_reverse_tag_dict(self):
1660- """Returns a dict with revisions as keys
1661- and a list of tags for that revision as value"""
1662- d = self.get_tag_dict()
1663- rev = defaultdict(set)
1664- for key in d:
1665- rev[d[key]].add(key)
1666- return rev
1667-
1668 def set_tag(self, name, revid):
1669 self._tag_dict[name] = revid
1670
1671@@ -385,11 +302,11 @@
1672 def _set_tag_dict(self, result):
1673 self._tag_dict = dict(result.items())
1674
1675- def merge_to(self, to_tags, overwrite=False, ignore_master=False):
1676+ def merge_to(self, to_tags, overwrite=False, ignore_master=False, selector=None):
1677 source_dict = self.get_tag_dict()
1678 dest_dict = to_tags.get_tag_dict()
1679 result, updates, conflicts = _reconcile_tags(
1680- source_dict, dest_dict, overwrite)
1681+ source_dict, dest_dict, overwrite, selector)
1682 if result != dest_dict:
1683 to_tags._set_tag_dict(result)
1684 return updates, conflicts
1685@@ -441,3 +358,6 @@
1686 tag_sort_methods.register("alpha", sort_alpha, 'Sort tags lexicographically.')
1687 tag_sort_methods.register("time", sort_time, 'Sort tags chronologically.')
1688 tag_sort_methods.default_key = "natural"
1689+
1690+
1691+InterTags.register_optimiser(InterTags)
1692
1693=== modified file 'breezy/tests/per_branch/test_pull.py'
1694--- breezy/tests/per_branch/test_pull.py 2018-11-17 20:50:40 +0000
1695+++ breezy/tests/per_branch/test_pull.py 2020-02-21 04:09:29 +0000
1696@@ -118,7 +118,7 @@
1697 self.assertEqual(p1, result.old_revid)
1698 self.assertEqual(2, result.new_revno)
1699 self.assertEqual(m1, result.new_revid)
1700- self.assertEqual([], result.tag_conflicts)
1701+ self.assertEqual([], list(result.tag_conflicts))
1702
1703 def test_pull_overwrite(self):
1704 tree_a = self.make_branch_and_tree('tree_a')
1705
1706=== modified file 'breezy/tests/per_branch/test_tags.py'
1707--- breezy/tests/per_branch/test_tags.py 2019-02-02 21:58:23 +0000
1708+++ breezy/tests/per_branch/test_tags.py 2020-02-21 04:09:29 +0000
1709@@ -142,6 +142,21 @@
1710 self.assertEqual(updates, {})
1711 self.assertEqual(b2.tags.lookup_tag('conflicts'), revid2)
1712
1713+ def test_merge_tags_selector(self):
1714+ b1, [revid, revid1] = self.make_branch_with_revision_tuple('b1', 2)
1715+ w2 = b1.controldir.sprout('b2', revision_id=revid).open_workingtree()
1716+ revid2 = w2.commit('revision 2')
1717+ b2 = w2.branch
1718+ # if there are tags in the source and not the destination, then they
1719+ # just go across
1720+ b1.tags.set_tag('tag1', revid)
1721+ b1.tags.set_tag('tag2', revid2)
1722+ updates, conflicts = b1.tags.merge_to(b2.tags, selector=lambda x: x == 'tag1')
1723+ self.assertEqual({'tag1': revid}, updates)
1724+ self.assertEqual(set(), set(conflicts))
1725+ self.assertEqual(b2.tags.lookup_tag('tag1'), revid)
1726+ self.assertRaises(errors.NoSuchTag, b2.tags.lookup_tag, 'tag2')
1727+
1728 def test_unicode_tag(self):
1729 tag_name = u'\u3070'
1730 b1, [revid] = self.make_branch_with_revision_tuple('b', 1)
1731
1732=== modified file 'breezy/tests/per_controldir/test_push.py'
1733--- breezy/tests/per_controldir/test_push.py 2019-02-15 03:42:06 +0000
1734+++ breezy/tests/per_controldir/test_push.py 2020-02-21 04:09:29 +0000
1735@@ -99,3 +99,14 @@
1736 self.assertEqual(2, result.branch_push_result.new_revno)
1737 self.assertEqual(tree.branch.base, result.source_branch.base)
1738 self.assertEqual(dir.open_branch().base, result.target_branch.base)
1739+
1740+ def test_push_tag_selector(self):
1741+ tree, rev1 = self.create_simple_tree()
1742+ try:
1743+ tree.branch.tags.set_tag('tag1', rev1)
1744+ except TagsNotSupported:
1745+ raise TestNotApplicable('tags not supported')
1746+ tree.branch.tags.set_tag('tag2', rev1)
1747+ dir = self.make_repository('dir').controldir
1748+ dir.push_branch(tree.branch, tag_selector=lambda x: x == 'tag1')
1749+ self.assertEqual({'tag1': rev1}, dir.open_branch().tags.get_tag_dict())
1750
1751=== modified file 'breezy/tests/per_interbranch/test_pull.py'
1752--- breezy/tests/per_interbranch/test_pull.py 2018-11-11 04:08:32 +0000
1753+++ breezy/tests/per_interbranch/test_pull.py 2020-02-21 04:09:29 +0000
1754@@ -144,7 +144,7 @@
1755 self.assertEqual(p1, result.old_revid)
1756 self.assertEqual(2, result.new_revno)
1757 self.assertEqual(m1, result.new_revid)
1758- self.assertEqual([], result.tag_conflicts)
1759+ self.assertEqual([], list(result.tag_conflicts))
1760
1761 def test_pull_overwrite(self):
1762 tree_a = self.make_from_branch_and_tree('tree_a')
1763@@ -180,6 +180,26 @@
1764 self.assertEqual(tree_b.branch.last_revision(),
1765 tree_a.branch.last_revision())
1766
1767+ def test_pull_tag_selector(self):
1768+ if not self.branch_format_from.supports_tags():
1769+ raise TestNotApplicable('from format does not support tags')
1770+ if not self.branch_format_to.supports_tags():
1771+ raise TestNotApplicable('to format does not support tags')
1772+ tree_a = self.make_from_branch_and_tree('tree_a')
1773+ revid1 = tree_a.commit('message 1')
1774+ try:
1775+ tree_b = self.sprout_to(
1776+ tree_a.controldir, 'tree_b').open_workingtree()
1777+ except errors.NoRoundtrippingSupport:
1778+ raise TestNotApplicable(
1779+ 'lossless push between %r and %r not supported' %
1780+ (self.branch_format_from, self.branch_format_to))
1781+ tree_b.branch.tags.set_tag('tag1', revid1)
1782+ tree_b.branch.tags.set_tag('tag2', revid1)
1783+ tree_b.branch.get_config_stack().set('branch.fetch_tags', True)
1784+ tree_a.pull(tree_b.branch, tag_selector=lambda x: x == 'tag1')
1785+ self.assertEqual({'tag1': revid1}, tree_a.branch.tags.get_tag_dict())
1786+
1787
1788 class TestPullHook(TestCaseWithInterBranch):
1789
1790
1791=== modified file 'breezy/tests/per_interbranch/test_push.py'
1792--- breezy/tests/per_interbranch/test_push.py 2020-02-07 02:14:30 +0000
1793+++ breezy/tests/per_interbranch/test_push.py 2020-02-21 04:09:29 +0000
1794@@ -374,6 +374,26 @@
1795 self.overrideAttr(SmartServerRepositoryGetParentMap,
1796 'no_extra_results', True)
1797
1798+ def test_push_tag_selector(self):
1799+ if not self.branch_format_from.supports_tags():
1800+ raise tests.TestNotApplicable('from format does not support tags')
1801+ if not self.branch_format_to.supports_tags():
1802+ raise tests.TestNotApplicable('to format does not support tags')
1803+ tree_a = self.make_from_branch_and_tree('tree_a')
1804+ revid1 = tree_a.commit('message 1')
1805+ try:
1806+ tree_b = self.sprout_to(
1807+ tree_a.controldir, 'tree_b').open_workingtree()
1808+ except errors.NoRoundtrippingSupport:
1809+ raise tests.TestNotApplicable(
1810+ 'lossless push between %r and %r not supported' %
1811+ (self.branch_format_from, self.branch_format_to))
1812+ tree_b.branch.tags.set_tag('tag1', revid1)
1813+ tree_b.branch.tags.set_tag('tag2', revid1)
1814+ tree_b.branch.get_config_stack().set('branch.fetch_tags', True)
1815+ tree_b.branch.push(tree_a.branch, tag_selector=lambda x: x == 'tag1')
1816+ self.assertEqual({'tag1': revid1}, tree_a.branch.tags.get_tag_dict())
1817+
1818
1819 class TestPushHook(TestCaseWithInterBranch):
1820
1821
1822=== modified file 'breezy/tests/test_foreign.py'
1823--- breezy/tests/test_foreign.py 2018-11-25 20:44:56 +0000
1824+++ breezy/tests/test_foreign.py 2020-02-21 04:09:29 +0000
1825@@ -171,7 +171,7 @@
1826 def is_compatible(source, target):
1827 return isinstance(target, DummyForeignVcsBranch)
1828
1829- def push(self, overwrite=False, stop_revision=None, lossy=False):
1830+ def push(self, overwrite=False, stop_revision=None, lossy=False, tag_selector=None):
1831 if not lossy:
1832 raise errors.NoRoundtrippingSupport(self.source, self.target)
1833 result = branch.BranchPushResult()
1834
1835=== modified file 'breezy/tests/test_tag.py'
1836--- breezy/tests/test_tag.py 2019-02-01 16:29:45 +0000
1837+++ breezy/tests/test_tag.py 2020-02-21 04:09:29 +0000
1838@@ -22,10 +22,12 @@
1839 errors,
1840 )
1841 from breezy.tag import (
1842- BasicTags,
1843 DisabledTags,
1844 MemoryTags,
1845 )
1846+from breezy.bzr.tag import (
1847+ BasicTags,
1848+ )
1849 from breezy.tests import (
1850 TestCase,
1851 TestCaseWithTransport,
1852@@ -120,6 +122,17 @@
1853 self.assertEqual({u'tag-2': b'z'}, updates)
1854 self.assertEqual(b'z', b.tags.lookup_tag('tag-2'))
1855
1856+ def test_merge_to_with_selector(self):
1857+ a = self.make_branch_supporting_tags('a')
1858+ b = self.make_branch_supporting_tags('b')
1859+ # simple merge
1860+ a.tags.set_tag('tag-1', b'x')
1861+ a.tags.set_tag('tag-2', b'y')
1862+ updates, conflicts = a.tags.merge_to(b.tags, selector=lambda x: x == 'tag-1')
1863+ self.assertEqual(list(conflicts), [])
1864+ self.assertEqual({u'tag-1': b'x'}, updates)
1865+ self.assertRaises(errors.NoSuchTag, b.tags.lookup_tag, 'tag-2')
1866+
1867
1868 class TestTagsInCheckouts(TestCaseWithTransport):
1869 """Tests for how tags are synchronised between the master and child branch
1870
1871=== modified file 'breezy/workingtree.py'
1872--- breezy/workingtree.py 2020-02-18 01:57:45 +0000
1873+++ breezy/workingtree.py 2020-02-21 04:09:29 +0000
1874@@ -819,13 +819,13 @@
1875
1876 def pull(self, source, overwrite=False, stop_revision=None,
1877 change_reporter=None, possible_transports=None, local=False,
1878- show_base=False):
1879+ show_base=False, tag_selector=None):
1880 with self.lock_write(), source.lock_read():
1881 old_revision_info = self.branch.last_revision_info()
1882 basis_tree = self.basis_tree()
1883 count = self.branch.pull(source, overwrite, stop_revision,
1884 possible_transports=possible_transports,
1885- local=local)
1886+ local=local, tag_selector=tag_selector)
1887 new_revision_info = self.branch.last_revision_info()
1888 if new_revision_info != old_revision_info:
1889 repository = self.branch.repository

Subscribers

People subscribed via source and target branches