Merge lp:~spiv/bzr/checkout-tags-propagation-603395-2.2 into lp:bzr/2.2
- checkout-tags-propagation-603395-2.2
- Merge into 2.2
Status: | Rejected |
---|---|
Rejected by: | Andrew Bennetts |
Proposed branch: | lp:~spiv/bzr/checkout-tags-propagation-603395-2.2 |
Merge into: | lp:bzr/2.2 |
Diff against target: |
423 lines (+272/-16) 8 files modified
NEWS (+13/-0) bzrlib/branch.py (+1/-1) bzrlib/builtins.py (+3/-1) bzrlib/tag.py (+69/-13) bzrlib/tests/blackbox/test_merge.py (+1/-1) bzrlib/tests/blackbox/test_tags.py (+12/-0) bzrlib/tests/per_branch/test_tags.py (+170/-0) bzrlib/tests/test_tag.py (+3/-0) |
To merge this branch: | bzr merge lp:~spiv/bzr/checkout-tags-propagation-603395-2.2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincent Ladeuil | Disapprove | ||
John A Meinel | Pending | ||
Review via email: mp+40406@code.launchpad.net |
This proposal supersedes a proposal from 2010-11-01.
Commit message
Description of the change
This is part of the fix for bug 603395. It makes BasicTags.merge_to also merge to the master branch, if there is one. This is consistent with e.g. set_tag. bzr-builddeb directly calls that API, and presumably expects merge_to in a checkout branch to update the master as well. Because bzr-builddeb does call this API directly I don't believe <https:/
I've dealt with my reservations on the original merge proposal: merge_to now accepts an optional kwarg, ignore_master, that cmd_merge uses to avoid prematurely propagating changes to the master. So I think this avoids any chance of regressing at all on #99137 (bzr revert will still not remove the new tags from the child branch, but that's not a regression). This does mean that external implementations of merge_to should be updated. I've documented the API change in NEWS, and made sure the callsite in bzr that does pass that kwarg falls back to not passing the kwarg. I've tested the fallback manually with bzr-svn (which has an external implementation of merge_to), and it appears to work as intended.
The tests take care to deal with all the permutations, including those that can only be triggered by an unfixed bzr operating on the checkout. See the test comments for details.
Andrew Bennetts (spiv) wrote : Posted in a previous version of this proposal | # |
John A Meinel (jameinel) wrote : Posted in a previous version of this proposal | # |
38 - dest_dict = to_tags.
39 - result, conflicts = self._reconcile
40 - overwrite)
41 - if result != dest_dict:
42 - to_tags.
43 + master = to_tags.
44 + try:
45 + if master is not None:
46 + master.lock_write()
47 + conflicts = self._merge_
48 + if master is not None:
49 + conflicts += self._merge_
50 + overwrite)
51 + finally:
52 + if master is not None:
53 + master.unlock()
Isn't this better written as:
master = to_tags.
if master is not None:
master.
try:
conflicts = ...
finally:
master.
Note that the way you wrote it, if we fail to take the write lock in the first place (no such permission), we end up still calling unlock.
I think you wrote it this way because you check the conflicts anyway, but the code looks clearer to me if you just add an 'else: conflicts = ...' to the above statement.
I suppose either way it is a bit clumsy, since you want the merge to fail immediately if you can't propagate the tags to the master branch before you try to merge to the local one...
I think the logic is otherwise ok.
Andrew Bennetts (spiv) wrote : Posted in a previous version of this proposal | # |
John A Meinel wrote:
[...]
> Note that the way you wrote it, if we fail to take the write lock in
> the first place (no such permission), we end up still calling unlock.
Good point!
> I think you wrote it this way because you check the conflicts anyway,
> but the code looks clearer to me if you just add an 'else: conflicts =
> ...' to the above statement.
>
> I suppose either way it is a bit clumsy, since you want the merge to
> fail immediately if you can't propagate the tags to the master branch
> before you try to merge to the local one...
Really what I want is to use add_cleanup. It seemed like it was perhaps
overkill, so I didn't do that initially. But this review makes it clear
to me that it really would be worthwhile, despite incurring a larger
diff.
Vincent Ladeuil (vila) wrote : | # |
Our policy for stable branches is a strict 'no API changes'.
I realize you have gone very far in ensuring compatibility and that this impacts UDD but this still doesn't justify breaking our policy IMHO.
Are there some other ways to propagate the tags to the master branch without this patch ?
I'm otherwise +1 for landing this on trunk.
Vincent Ladeuil (vila) wrote : | # |
More thoughts on this as I realize it's always frustrating to have a mp rejected.
My concerns here of course are: what if we break something ? Would that means we'll get into a whack-a-mole frenzy updating bzr and plugins introducing even more problems ?
Instead of focusing on the right fix on trunk...
What are your thoughts here and what do you plan to do next ?
Is there some way we can have some real-world testing here ? May be asking Jelmer for bzr-svn fallouts ? Do we have a clear understanding of *which* plugins are (or could be) impacted there ?
John A Meinel (jameinel) wrote : | # |
I think I'm happy with the change, but I would agree with Vincent's hesitancy to land it in 2.2, vs just landing it in trunk.
Preview Diff
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2010-11-08 06:10:41 +0000 | |||
3 | +++ NEWS 2010-11-09 06:33:49 +0000 | |||
4 | @@ -42,6 +42,11 @@ | |||
5 | 42 | root. Including the one that wasn't present in 2.1. | 42 | root. Including the one that wasn't present in 2.1. |
6 | 43 | (Vincent Ladeuil, #646133) | 43 | (Vincent Ladeuil, #646133) |
7 | 44 | 44 | ||
8 | 45 | * The ``branch.tags.merge_to(target_branch)`` API used by plugins such as | ||
9 | 46 | ``bzr-builddeb`` now propagates changes to the master branch of the | ||
10 | 47 | target branch (if there is one). This makes it consistent with the | ||
11 | 48 | other tag APIs. (Andrew Bennetts, #603395) | ||
12 | 49 | |||
13 | 45 | * Using bzr with `lp:` urls behind an http proxy should work. | 50 | * Using bzr with `lp:` urls behind an http proxy should work. |
14 | 46 | (Robert Collins, #558343) | 51 | (Robert Collins, #558343) |
15 | 47 | 52 | ||
16 | @@ -58,6 +63,14 @@ | |||
17 | 58 | API Changes | 63 | API Changes |
18 | 59 | *********** | 64 | *********** |
19 | 60 | 65 | ||
20 | 66 | * The ``merge_to`` method of ``branch.tags`` objects is now expected to | ||
21 | 67 | take a ``ignore_master=False`` keyword parameter. Plugins that | ||
22 | 68 | implement this method should add that parameter to their signature. | ||
23 | 69 | Callsites in bzr itself that pass that flag will still try to work with | ||
24 | 70 | the old signature (they catch TypeError and retry without the flag), but | ||
25 | 71 | this support is deprecated and will be removed eventually. | ||
26 | 72 | (Andrew Bennetts, #603395) | ||
27 | 73 | |||
28 | 61 | Internals | 74 | Internals |
29 | 62 | ********* | 75 | ********* |
30 | 63 | 76 | ||
31 | 64 | 77 | ||
32 | === modified file 'bzrlib/branch.py' | |||
33 | --- bzrlib/branch.py 2010-08-13 07:32:06 +0000 | |||
34 | +++ bzrlib/branch.py 2010-11-09 06:33:49 +0000 | |||
35 | @@ -3326,7 +3326,7 @@ | |||
36 | 3326 | if isinstance(format, remote.RemoteBranchFormat): | 3326 | if isinstance(format, remote.RemoteBranchFormat): |
37 | 3327 | format._ensure_real() | 3327 | format._ensure_real() |
38 | 3328 | return format._custom_format | 3328 | return format._custom_format |
40 | 3329 | return format | 3329 | return format |
41 | 3330 | 3330 | ||
42 | 3331 | @needs_write_lock | 3331 | @needs_write_lock |
43 | 3332 | def copy_content_into(self, revision_id=None): | 3332 | def copy_content_into(self, revision_id=None): |
44 | 3333 | 3333 | ||
45 | === modified file 'bzrlib/builtins.py' | |||
46 | --- bzrlib/builtins.py 2010-07-28 07:05:19 +0000 | |||
47 | +++ bzrlib/builtins.py 2010-11-09 06:33:49 +0000 | |||
48 | @@ -3980,7 +3980,9 @@ | |||
49 | 3980 | if ((remember or tree.branch.get_submit_branch() is None) and | 3980 | if ((remember or tree.branch.get_submit_branch() is None) and |
50 | 3981 | user_location is not None): | 3981 | user_location is not None): |
51 | 3982 | tree.branch.set_submit_branch(other_branch.base) | 3982 | tree.branch.set_submit_branch(other_branch.base) |
53 | 3983 | _merge_tags_if_possible(other_branch, tree.branch) | 3983 | # Merge tags (but don't set them in the master branch yet, the user |
54 | 3984 | # might revert this merge). Commit will propagate them. | ||
55 | 3985 | _merge_tags_if_possible(other_branch, tree.branch, ignore_master=True) | ||
56 | 3984 | merger = _mod_merge.Merger.from_revision_ids(pb, tree, | 3986 | merger = _mod_merge.Merger.from_revision_ids(pb, tree, |
57 | 3985 | other_revision_id, base_revision_id, other_branch, base_branch) | 3987 | other_revision_id, base_revision_id, other_branch, base_branch) |
58 | 3986 | if other_path != '': | 3988 | if other_path != '': |
59 | 3987 | 3989 | ||
60 | === modified file 'bzrlib/tag.py' | |||
61 | --- bzrlib/tag.py 2010-03-25 09:39:03 +0000 | |||
62 | +++ bzrlib/tag.py 2010-11-09 06:33:49 +0000 | |||
63 | @@ -28,7 +28,9 @@ | |||
64 | 28 | 28 | ||
65 | 29 | from bzrlib import ( | 29 | from bzrlib import ( |
66 | 30 | bencode, | 30 | bencode, |
67 | 31 | cleanup, | ||
68 | 31 | errors, | 32 | errors, |
69 | 33 | symbol_versioning, | ||
70 | 32 | trace, | 34 | trace, |
71 | 33 | ) | 35 | ) |
72 | 34 | 36 | ||
73 | @@ -57,7 +59,7 @@ | |||
74 | 57 | lookup_tag = _not_supported | 59 | lookup_tag = _not_supported |
75 | 58 | delete_tag = _not_supported | 60 | delete_tag = _not_supported |
76 | 59 | 61 | ||
78 | 60 | def merge_to(self, to_tags, overwrite=False): | 62 | def merge_to(self, to_tags, overwrite=False, ignore_master=False): |
79 | 61 | # we never have anything to copy | 63 | # we never have anything to copy |
80 | 62 | pass | 64 | pass |
81 | 63 | 65 | ||
82 | @@ -177,7 +179,7 @@ | |||
83 | 177 | raise ValueError("failed to deserialize tag dictionary %r: %s" | 179 | raise ValueError("failed to deserialize tag dictionary %r: %s" |
84 | 178 | % (tag_content, e)) | 180 | % (tag_content, e)) |
85 | 179 | 181 | ||
87 | 180 | def merge_to(self, to_tags, overwrite=False): | 182 | def merge_to(self, to_tags, overwrite=False, ignore_master=False): |
88 | 181 | """Copy tags between repositories if necessary and possible. | 183 | """Copy tags between repositories if necessary and possible. |
89 | 182 | 184 | ||
90 | 183 | This method has common command-line behaviour about handling | 185 | This method has common command-line behaviour about handling |
91 | @@ -188,11 +190,19 @@ | |||
92 | 188 | 190 | ||
93 | 189 | :param to_tags: Branch to receive these tags | 191 | :param to_tags: Branch to receive these tags |
94 | 190 | :param overwrite: Overwrite conflicting tags in the target branch | 192 | :param overwrite: Overwrite conflicting tags in the target branch |
95 | 193 | :param ignore_master: Do not modify the tags in the target's master | ||
96 | 194 | branch (if any). Default is false (so the master will be updated). | ||
97 | 195 | New in bzr 2.2.2 and bzr 2.3. | ||
98 | 191 | 196 | ||
99 | 192 | :returns: A list of tags that conflicted, each of which is | 197 | :returns: A list of tags that conflicted, each of which is |
100 | 193 | (tagname, source_target, dest_target), or None if no copying was | 198 | (tagname, source_target, dest_target), or None if no copying was |
101 | 194 | done. | 199 | done. |
102 | 195 | """ | 200 | """ |
103 | 201 | operation = cleanup.OperationWithCleanups(self._merge_to_operation) | ||
104 | 202 | return operation.run(to_tags, overwrite, ignore_master) | ||
105 | 203 | |||
106 | 204 | def _merge_to_operation(self, operation, to_tags, overwrite, ignore_master): | ||
107 | 205 | add_cleanup = operation.add_cleanup | ||
108 | 196 | if self.branch == to_tags.branch: | 206 | if self.branch == to_tags.branch: |
109 | 197 | return | 207 | return |
110 | 198 | if not self.branch.supports_tags(): | 208 | if not self.branch.supports_tags(): |
111 | @@ -203,15 +213,40 @@ | |||
112 | 203 | # no tags in the source, and we don't want to clobber anything | 213 | # no tags in the source, and we don't want to clobber anything |
113 | 204 | # that's in the destination | 214 | # that's in the destination |
114 | 205 | return | 215 | return |
124 | 206 | to_tags.branch.lock_write() | 216 | # We merge_to both master and child individually. |
125 | 207 | try: | 217 | # |
126 | 208 | dest_dict = to_tags.get_tag_dict() | 218 | # It's possible for master and child to have differing sets of |
127 | 209 | result, conflicts = self._reconcile_tags(source_dict, dest_dict, | 219 | # tags, in which case it's possible to have different sets of |
128 | 210 | overwrite) | 220 | # conflicts. We report the union of both conflict sets. In |
129 | 211 | if result != dest_dict: | 221 | # that case it's likely the child and master have accepted |
130 | 212 | to_tags._set_tag_dict(result) | 222 | # different tags from the source, which may be a surprising result, but |
131 | 213 | finally: | 223 | # the best we can do in the circumstances. |
132 | 214 | to_tags.branch.unlock() | 224 | # |
133 | 225 | # Ideally we'd improve this API to report the different conflicts | ||
134 | 226 | # more clearly to the caller, but we don't want to break plugins | ||
135 | 227 | # such as bzr-builddeb that use this API. | ||
136 | 228 | add_cleanup(to_tags.branch.lock_write().unlock) | ||
137 | 229 | if ignore_master: | ||
138 | 230 | master = None | ||
139 | 231 | else: | ||
140 | 232 | master = to_tags.branch.get_master_branch() | ||
141 | 233 | if master is not None: | ||
142 | 234 | add_cleanup(master.lock_write().unlock) | ||
143 | 235 | conflicts = self._merge_to(to_tags, source_dict, overwrite) | ||
144 | 236 | if master is not None: | ||
145 | 237 | conflicts += self._merge_to(master.tags, source_dict, | ||
146 | 238 | overwrite) | ||
147 | 239 | # We use set() to remove any duplicate conflicts from the master | ||
148 | 240 | # branch. We then use list() to keep the behaviour as close to 2.2.1 | ||
149 | 241 | # and earlier as possible, to minimise potential compatibility issues. | ||
150 | 242 | return list(set(conflicts)) | ||
151 | 243 | |||
152 | 244 | def _merge_to(self, to_tags, source_dict, overwrite): | ||
153 | 245 | dest_dict = to_tags.get_tag_dict() | ||
154 | 246 | result, conflicts = self._reconcile_tags(source_dict, dest_dict, | ||
155 | 247 | overwrite) | ||
156 | 248 | if result != dest_dict: | ||
157 | 249 | to_tags._set_tag_dict(result) | ||
158 | 215 | return conflicts | 250 | return conflicts |
159 | 216 | 251 | ||
160 | 217 | def rename_revisions(self, rename_map): | 252 | def rename_revisions(self, rename_map): |
161 | @@ -249,6 +284,27 @@ | |||
162 | 249 | return result, conflicts | 284 | return result, conflicts |
163 | 250 | 285 | ||
164 | 251 | 286 | ||
167 | 252 | def _merge_tags_if_possible(from_branch, to_branch): | 287 | def _merge_tags_if_possible(from_branch, to_branch, ignore_master=False): |
168 | 253 | from_branch.tags.merge_to(to_branch.tags) | 288 | # Try hard to support merge_to implementations that don't expect |
169 | 289 | # 'ignore_master' (new in bzr 2.2.2/2.3). First, if the flag isn't set | ||
170 | 290 | # then we can safely avoid passing ignore_master at all. | ||
171 | 291 | if not ignore_master: | ||
172 | 292 | from_branch.tags.merge_to(to_branch.tags) | ||
173 | 293 | return | ||
174 | 294 | # If the flag is set, try to pass it, but be ready to catch TypeError. | ||
175 | 295 | try: | ||
176 | 296 | from_branch.tags.merge_to(to_branch.tags, ignore_master=ignore_master) | ||
177 | 297 | except TypeError: | ||
178 | 298 | # Probably this implementation of 'merge_to' is from a plugin that | ||
179 | 299 | # doesn't expect the 'ignore_master' keyword argument (e.g. bzr-svn | ||
180 | 300 | # 1.0.4). There's a small risk that the TypeError is actually caused | ||
181 | 301 | # by a completely different problem (which is why we don't catch it for | ||
182 | 302 | # the ignore_master=False case), but even then there's probably no harm | ||
183 | 303 | # in calling a second time. | ||
184 | 304 | symbol_versioning.warn( | ||
185 | 305 | symbol_versioning.deprecated_in((2,2,2)) % ( | ||
186 | 306 | "Tags.merge_to (of %r) that doesn't accept ignore_master kwarg" | ||
187 | 307 | % (from_branch.tags,),), | ||
188 | 308 | DeprecationWarning) | ||
189 | 309 | from_branch.tags.merge_to(to_branch.tags) | ||
190 | 254 | 310 | ||
191 | 255 | 311 | ||
192 | === modified file 'bzrlib/tests/blackbox/test_merge.py' | |||
193 | --- bzrlib/tests/blackbox/test_merge.py 2010-05-20 18:23:17 +0000 | |||
194 | +++ bzrlib/tests/blackbox/test_merge.py 2010-11-09 06:33:49 +0000 | |||
195 | @@ -23,9 +23,9 @@ | |||
196 | 23 | 23 | ||
197 | 24 | from bzrlib import ( | 24 | from bzrlib import ( |
198 | 25 | branch, | 25 | branch, |
199 | 26 | branchbuilder, | ||
200 | 26 | bzrdir, | 27 | bzrdir, |
201 | 27 | conflicts, | 28 | conflicts, |
202 | 28 | errors, | ||
203 | 29 | merge_directive, | 29 | merge_directive, |
204 | 30 | osutils, | 30 | osutils, |
205 | 31 | tests, | 31 | tests, |
206 | 32 | 32 | ||
207 | === modified file 'bzrlib/tests/blackbox/test_tags.py' | |||
208 | --- bzrlib/tests/blackbox/test_tags.py 2010-11-01 05:21:13 +0000 | |||
209 | +++ bzrlib/tests/blackbox/test_tags.py 2010-11-09 06:33:49 +0000 | |||
210 | @@ -132,6 +132,18 @@ | |||
211 | 132 | builder.build_commit(message='Commit in fork.', rev_id='fork-1') | 132 | builder.build_commit(message='Commit in fork.', rev_id='fork-1') |
212 | 133 | return fork | 133 | return fork |
213 | 134 | 134 | ||
214 | 135 | def test_merge_without_commit_does_not_propagate_tags_to_master(self): | ||
215 | 136 | """'bzr merge' alone does not propagate tags to a master branch. | ||
216 | 137 | |||
217 | 138 | (If the user runs 'bzr commit', then that is when the tags from the | ||
218 | 139 | merge are propagated.) | ||
219 | 140 | """ | ||
220 | 141 | master, child = self.make_master_and_checkout() | ||
221 | 142 | fork = self.make_fork(master) | ||
222 | 143 | fork.tags.set_tag('new-tag', fork.last_revision()) | ||
223 | 144 | self.run_bzr(['merge', '../fork'], working_dir='child') | ||
224 | 145 | self.assertEqual({}, master.tags.get_tag_dict()) | ||
225 | 146 | |||
226 | 135 | def test_commit_in_heavyweight_checkout_copies_tags_to_master(self): | 147 | def test_commit_in_heavyweight_checkout_copies_tags_to_master(self): |
227 | 136 | master, child = self.make_master_and_checkout() | 148 | master, child = self.make_master_and_checkout() |
228 | 137 | fork = self.make_fork(master) | 149 | fork = self.make_fork(master) |
229 | 138 | 150 | ||
230 | === modified file 'bzrlib/tests/per_branch/test_tags.py' | |||
231 | --- bzrlib/tests/per_branch/test_tags.py 2010-03-12 02:14:56 +0000 | |||
232 | +++ bzrlib/tests/per_branch/test_tags.py 2010-11-09 06:33:49 +0000 | |||
233 | @@ -144,6 +144,176 @@ | |||
234 | 144 | b1.tags.merge_to(b2.tags) | 144 | b1.tags.merge_to(b2.tags) |
235 | 145 | 145 | ||
236 | 146 | 146 | ||
237 | 147 | class TestTagsMergeToInCheckouts(per_branch.TestCaseWithBranch): | ||
238 | 148 | """Tests for checkout.branch.tags.merge_to. | ||
239 | 149 | |||
240 | 150 | In particular this exercises variations in tag conflicts in the master | ||
241 | 151 | branch and/or the checkout (child). It may seem strange to have different | ||
242 | 152 | tags in the child and master, but 'bzr merge' intentionally updates the | ||
243 | 153 | child and not the master (instead the next 'bzr commit', if the user | ||
244 | 154 | decides to commit, will update the master). Also, merge_to in bzr < 2.2.2 | ||
245 | 155 | didn't propagate changes to the master, and current bzr versions may find | ||
246 | 156 | themselves operating on checkouts touched by older bzrs | ||
247 | 157 | |||
248 | 158 | So we need to make sure bzr copes gracefully with differing tags in the | ||
249 | 159 | master versus the child. | ||
250 | 160 | |||
251 | 161 | See also <https://bugs.launchpad.net/bzr/+bug/603395>. | ||
252 | 162 | """ | ||
253 | 163 | |||
254 | 164 | def setUp(self): | ||
255 | 165 | super(TestTagsMergeToInCheckouts, self).setUp() | ||
256 | 166 | branch1 = self.make_branch('tags-probe') | ||
257 | 167 | if not branch1._format.supports_tags(): | ||
258 | 168 | raise tests.TestSkipped( | ||
259 | 169 | "format %s doesn't support tags" % branch1._format) | ||
260 | 170 | branch2 = self.make_branch('bind-probe') | ||
261 | 171 | try: | ||
262 | 172 | branch2.bind(branch1) | ||
263 | 173 | except errors.UpgradeRequired: | ||
264 | 174 | raise tests.TestNotApplicable( | ||
265 | 175 | "format %s doesn't support bound branches" % branch2._format) | ||
266 | 176 | |||
267 | 177 | def test_merge_to_propagates_tags(self): | ||
268 | 178 | """merge_to(child) also merges tags to the master.""" | ||
269 | 179 | master = self.make_branch('master') | ||
270 | 180 | other = self.make_branch('other') | ||
271 | 181 | other.tags.set_tag('foo', 'rev-1') | ||
272 | 182 | child = self.make_branch('child') | ||
273 | 183 | child.bind(master) | ||
274 | 184 | child.update() | ||
275 | 185 | other.tags.merge_to(child.tags) | ||
276 | 186 | self.assertEquals('rev-1', child.tags.lookup_tag('foo')) | ||
277 | 187 | self.assertEquals('rev-1', master.tags.lookup_tag('foo')) | ||
278 | 188 | |||
279 | 189 | def test_ignore_master_disables_tag_propagation(self): | ||
280 | 190 | """merge_to(child, ignore_master=True) does not merge tags to the | ||
281 | 191 | master. | ||
282 | 192 | """ | ||
283 | 193 | master = self.make_branch('master') | ||
284 | 194 | other = self.make_branch('other') | ||
285 | 195 | other.tags.set_tag('foo', 'rev-1') | ||
286 | 196 | child = self.make_branch('child') | ||
287 | 197 | child.bind(master) | ||
288 | 198 | child.update() | ||
289 | 199 | other.tags.merge_to(child.tags, ignore_master=True) | ||
290 | 200 | self.assertEquals('rev-1', child.tags.lookup_tag('foo')) | ||
291 | 201 | self.assertRaises(errors.NoSuchTag, master.tags.lookup_tag, 'foo') | ||
292 | 202 | |||
293 | 203 | def test_merge_to_overwrite_conflict_in_master(self): | ||
294 | 204 | """merge_to(child, overwrite=True) overwrites any conflicting tags in | ||
295 | 205 | the master. | ||
296 | 206 | """ | ||
297 | 207 | master = self.make_branch('master') | ||
298 | 208 | other = self.make_branch('other') | ||
299 | 209 | other.tags.set_tag('foo', 'rev-1') | ||
300 | 210 | child = self.make_branch('child') | ||
301 | 211 | child.bind(master) | ||
302 | 212 | child.update() | ||
303 | 213 | master.tags.set_tag('foo', 'rev-2') | ||
304 | 214 | tag_conflicts = other.tags.merge_to(child.tags, overwrite=True) | ||
305 | 215 | self.assertEquals('rev-1', child.tags.lookup_tag('foo')) | ||
306 | 216 | self.assertEquals('rev-1', master.tags.lookup_tag('foo')) | ||
307 | 217 | self.assertLength(0, tag_conflicts) | ||
308 | 218 | |||
309 | 219 | def test_merge_to_overwrite_conflict_in_child_and_master(self): | ||
310 | 220 | """merge_to(child, overwrite=True) overwrites any conflicting tags in | ||
311 | 221 | both the child and the master. | ||
312 | 222 | """ | ||
313 | 223 | master = self.make_branch('master') | ||
314 | 224 | master.tags.set_tag('foo', 'rev-2') | ||
315 | 225 | other = self.make_branch('other') | ||
316 | 226 | other.tags.set_tag('foo', 'rev-1') | ||
317 | 227 | child = self.make_branch('child') | ||
318 | 228 | child.bind(master) | ||
319 | 229 | child.update() | ||
320 | 230 | tag_conflicts = other.tags.merge_to(child.tags, overwrite=True) | ||
321 | 231 | self.assertEquals('rev-1', child.tags.lookup_tag('foo')) | ||
322 | 232 | self.assertEquals('rev-1', master.tags.lookup_tag('foo')) | ||
323 | 233 | self.assertLength(0, tag_conflicts) | ||
324 | 234 | |||
325 | 235 | def test_merge_to_conflict_in_child_only(self): | ||
326 | 236 | """When new_tags.merge_to(child.tags) conflicts with the child but not | ||
327 | 237 | the master, a conflict is reported and the child receives the new tag. | ||
328 | 238 | """ | ||
329 | 239 | master = self.make_branch('master') | ||
330 | 240 | master.tags.set_tag('foo', 'rev-2') | ||
331 | 241 | other = self.make_branch('other') | ||
332 | 242 | other.tags.set_tag('foo', 'rev-1') | ||
333 | 243 | child = self.make_branch('child') | ||
334 | 244 | child.bind(master) | ||
335 | 245 | child.update() | ||
336 | 246 | master.tags.delete_tag('foo') | ||
337 | 247 | tag_conflicts = other.tags.merge_to(child.tags) | ||
338 | 248 | # Conflict in child, so it is unchanged. | ||
339 | 249 | self.assertEquals('rev-2', child.tags.lookup_tag('foo')) | ||
340 | 250 | # No conflict in the master, so the 'foo' tag equals other's value here. | ||
341 | 251 | self.assertEquals('rev-1', master.tags.lookup_tag('foo')) | ||
342 | 252 | # The conflict is reported. | ||
343 | 253 | self.assertEquals([(u'foo', 'rev-1', 'rev-2')], tag_conflicts) | ||
344 | 254 | |||
345 | 255 | def test_merge_to_conflict_in_master_only(self): | ||
346 | 256 | """When new_tags.merge_to(child.tags) conflicts with the master but not | ||
347 | 257 | the child, a conflict is reported and the child receives the new tag. | ||
348 | 258 | """ | ||
349 | 259 | master = self.make_branch('master') | ||
350 | 260 | other = self.make_branch('other') | ||
351 | 261 | other.tags.set_tag('foo', 'rev-1') | ||
352 | 262 | child = self.make_branch('child') | ||
353 | 263 | child.bind(master) | ||
354 | 264 | child.update() | ||
355 | 265 | master.tags.set_tag('foo', 'rev-2') | ||
356 | 266 | tag_conflicts = other.tags.merge_to(child.tags) | ||
357 | 267 | # No conflict in the child, so the 'foo' tag equals other's value here. | ||
358 | 268 | self.assertEquals('rev-1', child.tags.lookup_tag('foo')) | ||
359 | 269 | # Conflict in master, so it is unchanged. | ||
360 | 270 | self.assertEquals('rev-2', master.tags.lookup_tag('foo')) | ||
361 | 271 | # The conflict is reported. | ||
362 | 272 | self.assertEquals([(u'foo', 'rev-1', 'rev-2')], tag_conflicts) | ||
363 | 273 | |||
364 | 274 | def test_merge_to_same_conflict_in_master_and_child(self): | ||
365 | 275 | """When new_tags.merge_to(child.tags) conflicts the same way with the | ||
366 | 276 | master and the child a single conflict is reported. | ||
367 | 277 | """ | ||
368 | 278 | master = self.make_branch('master') | ||
369 | 279 | master.tags.set_tag('foo', 'rev-2') | ||
370 | 280 | other = self.make_branch('other') | ||
371 | 281 | other.tags.set_tag('foo', 'rev-1') | ||
372 | 282 | child = self.make_branch('child') | ||
373 | 283 | child.bind(master) | ||
374 | 284 | child.update() | ||
375 | 285 | tag_conflicts = other.tags.merge_to(child.tags) | ||
376 | 286 | # Both master and child conflict, so both stay as rev-2 | ||
377 | 287 | self.assertEquals('rev-2', child.tags.lookup_tag('foo')) | ||
378 | 288 | self.assertEquals('rev-2', master.tags.lookup_tag('foo')) | ||
379 | 289 | # The conflict is reported exactly once, even though it occurs in both | ||
380 | 290 | # master and child. | ||
381 | 291 | self.assertEquals([(u'foo', 'rev-1', 'rev-2')], tag_conflicts) | ||
382 | 292 | |||
383 | 293 | def test_merge_to_different_conflict_in_master_and_child(self): | ||
384 | 294 | """When new_tags.merge_to(child.tags) conflicts differently in the | ||
385 | 295 | master and the child both conflicts are reported. | ||
386 | 296 | """ | ||
387 | 297 | master = self.make_branch('master') | ||
388 | 298 | master.tags.set_tag('foo', 'rev-2') | ||
389 | 299 | other = self.make_branch('other') | ||
390 | 300 | other.tags.set_tag('foo', 'rev-1') | ||
391 | 301 | child = self.make_branch('child') | ||
392 | 302 | child.bind(master) | ||
393 | 303 | child.update() | ||
394 | 304 | # We use the private method _set_tag_dict because normally bzr tries to | ||
395 | 305 | # avoid this scenario. | ||
396 | 306 | child.tags._set_tag_dict({'foo': 'rev-3'}) | ||
397 | 307 | tag_conflicts = other.tags.merge_to(child.tags) | ||
398 | 308 | # Both master and child conflict, so both stay as they were. | ||
399 | 309 | self.assertEquals('rev-3', child.tags.lookup_tag('foo')) | ||
400 | 310 | self.assertEquals('rev-2', master.tags.lookup_tag('foo')) | ||
401 | 311 | # Both conflicts are reported. | ||
402 | 312 | self.assertEquals( | ||
403 | 313 | [(u'foo', 'rev-1', 'rev-2'), (u'foo', 'rev-1', 'rev-3')], | ||
404 | 314 | sorted(tag_conflicts)) | ||
405 | 315 | |||
406 | 316 | |||
407 | 147 | class TestUnsupportedTags(per_branch.TestCaseWithBranch): | 317 | class TestUnsupportedTags(per_branch.TestCaseWithBranch): |
408 | 148 | """Formats that don't support tags should give reasonable errors.""" | 318 | """Formats that don't support tags should give reasonable errors.""" |
409 | 149 | 319 | ||
410 | 150 | 320 | ||
411 | === modified file 'bzrlib/tests/test_tag.py' | |||
412 | --- bzrlib/tests/test_tag.py 2010-03-25 09:39:03 +0000 | |||
413 | +++ bzrlib/tests/test_tag.py 2010-11-09 06:33:49 +0000 | |||
414 | @@ -120,6 +120,9 @@ | |||
415 | 120 | 120 | ||
416 | 121 | 121 | ||
417 | 122 | class TestTagsInCheckouts(TestCaseWithTransport): | 122 | class TestTagsInCheckouts(TestCaseWithTransport): |
418 | 123 | """Tests for how tags are synchronised between the master and child branch | ||
419 | 124 | of a checkout. | ||
420 | 125 | """ | ||
421 | 123 | 126 | ||
422 | 124 | def test_update_tag_into_checkout(self): | 127 | def test_update_tag_into_checkout(self): |
423 | 125 | # checkouts are directly connected to the tags of their master branch: | 128 | # checkouts are directly connected to the tags of their master branch: |
I should point out: this does have the perhaps unwanted side-effect that "bzr merge" can cause tags to be added the master even if the next command is "bzr commit". That's really <https:/ /bugs.launchpad .net/bzr/ +bug/99137> (tags are "permanently" propagated by merge), and this patch is just making the overall behaviour more consistent. I can see an argument that undoing the accidental limiting of the the effect of bug 99137 might not be acceptable in a stable branch, though.
A compromise would be to add a new optional argument to merge_to (or perhaps a whole new method) that callers can use to explicitly enable/disable propagation. I'm not sure if it would be better to default to enabled (so that plugins like builddeb can automatically benefit), or disabled (to minimise the change in behaviour, and also perhaps to avoid needing to tweak the API layers between cmd_merge and br.tags.merge_to to pass the flag). With 'bzr merge' not propagating we'd then rely on <https:/ /code.launchpad .net/~spiv/ bzr/tags- commit- propagation- 603395- 2.2> to propagate at commit time rather than merge time.