Merge lp:~abentley/launchpad/incremental-diff-driveby into lp:launchpad
- incremental-diff-driveby
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | 11592 |
Proposed branch: | lp:~abentley/launchpad/incremental-diff-driveby |
Merge into: | lp:launchpad |
Diff against target: |
676 lines (+193/-130) 13 files modified
lib/lp/code/browser/branchmergeproposal.py (+8/-42) lib/lp/code/browser/tests/test_branchmergeproposal.py (+0/-60) lib/lp/code/configure.zcml (+2/-0) lib/lp/code/interfaces/branchmergeproposal.py (+9/-2) lib/lp/code/interfaces/revision.py (+3/-0) lib/lp/code/model/branchmergeproposal.py (+40/-1) lib/lp/code/model/directbranchcommit.py (+5/-2) lib/lp/code/model/revision.py (+8/-1) lib/lp/code/model/tests/test_branchmergeproposal.py (+67/-4) lib/lp/code/model/tests/test_diff.py (+15/-16) lib/lp/code/tests/helpers.py (+7/-2) lib/lp/code/tests/test_directbranchcommit.py (+18/-0) lib/lp/codehosting/bzrutils.py (+11/-0) |
To merge this branch: | bzr merge lp:~abentley/launchpad/incremental-diff-driveby |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | Approve | ||
Review via email: mp+36156@code.launchpad.net |
Commit message
Drive-bys to support incremental diffs.
Description of the change
= Summary =
A set of drivebys to support incremental diffs. Although incremental diffs
will be merged into db-devel, the drivebys will land in devel to reduce skew.
== Proposed fix ==
N/A
== Pre-implementation notes ==
Moving view code into the model was discussed with thumper.
== Implementation details ==
revision_end_date and getRevisionsSin
the model. getRevisionsSin
reimplementing it. _getNewerRevisions is extracted from
getRevisionsSin
bzrutils.
DirectBranchCommit allows merge parents to be specified.
DiffTestCase.
add_revision_
Revision.
the revision.
Literal tab characters are replaced with \t to please lint.
== Tests ==
bin/test -v -t test_commit_
== Demo and Q/A ==
None
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
715: E302 expected 2 blank lines, found 1
./lib/lp/
192: E231 missing whitespace after ','
194: E231 missing whitespace after ','
./lib/lp/
165: E301 expected 1 blank line, found 0
343: E301 expected 1 blank line, found 0
./lib/lp/
168: E301 expected 1 blank line, found 0
178: E301 expected 1 blank line, found 0
201: E202 whitespace before '}'
1441: E301 expected 1 blank line, found 0
Paul Hummer (rockstar) : | # |
Preview Diff
1 | === modified file 'lib/lp/code/browser/branchmergeproposal.py' | |||
2 | --- lib/lp/code/browser/branchmergeproposal.py 2010-08-24 10:45:57 +0000 | |||
3 | +++ lib/lp/code/browser/branchmergeproposal.py 2010-09-21 15:06:00 +0000 | |||
4 | @@ -32,7 +32,6 @@ | |||
5 | 32 | 'latest_proposals_for_each_branch', | 32 | 'latest_proposals_for_each_branch', |
6 | 33 | ] | 33 | ] |
7 | 34 | 34 | ||
8 | 35 | from collections import defaultdict | ||
9 | 36 | import operator | 35 | import operator |
10 | 37 | 36 | ||
11 | 38 | from lazr.delegates import delegates | 37 | from lazr.delegates import delegates |
12 | @@ -200,7 +199,7 @@ | |||
13 | 200 | 'Approved [Merge Failed]', | 199 | 'Approved [Merge Failed]', |
14 | 201 | BranchMergeProposalStatus.QUEUED : 'Queued', | 200 | BranchMergeProposalStatus.QUEUED : 'Queued', |
15 | 202 | BranchMergeProposalStatus.SUPERSEDED : 'Superseded' | 201 | BranchMergeProposalStatus.SUPERSEDED : 'Superseded' |
17 | 203 | } | 202 | } |
18 | 204 | return friendly_texts[self.context.queue_status] | 203 | return friendly_texts[self.context.queue_status] |
19 | 205 | 204 | ||
20 | 206 | @property | 205 | @property |
21 | @@ -212,8 +211,7 @@ | |||
22 | 212 | result = '' | 211 | result = '' |
23 | 213 | if self.context.queue_status in ( | 212 | if self.context.queue_status in ( |
24 | 214 | BranchMergeProposalStatus.CODE_APPROVED, | 213 | BranchMergeProposalStatus.CODE_APPROVED, |
27 | 215 | BranchMergeProposalStatus.REJECTED | 214 | BranchMergeProposalStatus.REJECTED): |
26 | 216 | ): | ||
28 | 217 | formatter = DateTimeFormatterAPI(self.context.date_reviewed) | 215 | formatter = DateTimeFormatterAPI(self.context.date_reviewed) |
29 | 218 | result = '%s %s' % ( | 216 | result = '%s %s' % ( |
30 | 219 | self.context.reviewer.displayname, | 217 | self.context.reviewer.displayname, |
31 | @@ -601,46 +599,15 @@ | |||
32 | 601 | """Location of page for commenting on this proposal.""" | 599 | """Location of page for commenting on this proposal.""" |
33 | 602 | return canonical_url(self.context, view_name='+comment') | 600 | return canonical_url(self.context, view_name='+comment') |
34 | 603 | 601 | ||
35 | 604 | @property | ||
36 | 605 | def revision_end_date(self): | ||
37 | 606 | """The cutoff date for showing revisions. | ||
38 | 607 | |||
39 | 608 | If the proposal has been merged, then we stop at the merged date. If | ||
40 | 609 | it is rejected, we stop at the reviewed date. For superseded | ||
41 | 610 | proposals, it should ideally use the non-existant date_last_modified, | ||
42 | 611 | but could use the last comment date. | ||
43 | 612 | """ | ||
44 | 613 | status = self.context.queue_status | ||
45 | 614 | if status == BranchMergeProposalStatus.MERGED: | ||
46 | 615 | return self.context.date_merged | ||
47 | 616 | if status == BranchMergeProposalStatus.REJECTED: | ||
48 | 617 | return self.context.date_reviewed | ||
49 | 618 | # Otherwise return None representing an open end date. | ||
50 | 619 | return None | ||
51 | 620 | |||
52 | 621 | def _getRevisionsSinceReviewStart(self): | ||
53 | 622 | """Get the grouped revisions since the review started.""" | ||
54 | 623 | # Work out the start of the review. | ||
55 | 624 | start_date = self.context.date_review_requested | ||
56 | 625 | if start_date is None: | ||
57 | 626 | start_date = self.context.date_created | ||
58 | 627 | source = DecoratedBranch(self.context.source_branch) | ||
59 | 628 | resultset = source.getMainlineBranchRevisions( | ||
60 | 629 | start_date, self.revision_end_date, oldest_first=True) | ||
61 | 630 | # Now group by date created. | ||
62 | 631 | groups = defaultdict(list) | ||
63 | 632 | for branch_revision, revision, revision_author in resultset: | ||
64 | 633 | groups[revision.date_created].append(branch_revision) | ||
65 | 634 | return [ | ||
66 | 635 | CodeReviewNewRevisions(revisions, date, source) | ||
67 | 636 | for date, revisions in groups.iteritems()] | ||
68 | 637 | |||
69 | 638 | @cachedproperty | 602 | @cachedproperty |
70 | 639 | def conversation(self): | 603 | def conversation(self): |
71 | 640 | """Return a conversation that is to be rendered.""" | 604 | """Return a conversation that is to be rendered.""" |
72 | 641 | # Sort the comments by date order. | 605 | # Sort the comments by date order. |
73 | 642 | comments = self._getRevisionsSinceReviewStart() | ||
74 | 643 | merge_proposal = self.context | 606 | merge_proposal = self.context |
75 | 607 | groups = merge_proposal.getRevisionsSinceReviewStart() | ||
76 | 608 | source = DecoratedBranch(merge_proposal.source_branch) | ||
77 | 609 | comments = [CodeReviewNewRevisions(list(revisions), date, source) | ||
78 | 610 | for date, revisions in groups] | ||
79 | 644 | while merge_proposal is not None: | 611 | while merge_proposal is not None: |
80 | 645 | from_superseded = merge_proposal != self.context | 612 | from_superseded = merge_proposal != self.context |
81 | 646 | comments.extend( | 613 | comments.extend( |
82 | @@ -946,7 +913,6 @@ | |||
83 | 946 | self.cancel_url = self.next_url | 913 | self.cancel_url = self.next_url |
84 | 947 | super(MergeProposalEditView, self).initialize() | 914 | super(MergeProposalEditView, self).initialize() |
85 | 948 | 915 | ||
86 | 949 | |||
87 | 950 | def _getRevisionId(self, data): | 916 | def _getRevisionId(self, data): |
88 | 951 | """Translate the revision number that was entered into a revision id. | 917 | """Translate the revision number that was entered into a revision id. |
89 | 952 | 918 | ||
90 | @@ -1473,8 +1439,8 @@ | |||
91 | 1473 | """Render an `IText` as XHTML using the webservice.""" | 1439 | """Render an `IText` as XHTML using the webservice.""" |
92 | 1474 | formatter = FormattersAPI | 1440 | formatter = FormattersAPI |
93 | 1475 | def renderer(value): | 1441 | def renderer(value): |
96 | 1476 | nomail = formatter(value).obfuscate_email() | 1442 | nomail = formatter(value).obfuscate_email() |
97 | 1477 | html = formatter(nomail).text_to_html() | 1443 | html = formatter(nomail).text_to_html() |
98 | 1478 | return html.encode('utf-8') | 1444 | return html.encode('utf-8') |
99 | 1479 | return renderer | 1445 | return renderer |
100 | 1480 | 1446 | ||
101 | 1481 | 1447 | ||
102 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' | |||
103 | --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-08-22 21:34:16 +0000 | |||
104 | +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-09-21 15:06:00 +0000 | |||
105 | @@ -12,7 +12,6 @@ | |||
106 | 12 | timedelta, | 12 | timedelta, |
107 | 13 | ) | 13 | ) |
108 | 14 | from difflib import unified_diff | 14 | from difflib import unified_diff |
109 | 15 | import operator | ||
110 | 16 | import unittest | 15 | import unittest |
111 | 17 | 16 | ||
112 | 18 | import pytz | 17 | import pytz |
113 | @@ -46,7 +45,6 @@ | |||
114 | 46 | PreviewDiff, | 45 | PreviewDiff, |
115 | 47 | StaticDiff, | 46 | StaticDiff, |
116 | 48 | ) | 47 | ) |
117 | 49 | from lp.code.tests.helpers import add_revision_to_branch | ||
118 | 50 | from lp.testing import ( | 48 | from lp.testing import ( |
119 | 51 | login_person, | 49 | login_person, |
120 | 52 | TestCaseWithFactory, | 50 | TestCaseWithFactory, |
121 | @@ -577,63 +575,6 @@ | |||
122 | 577 | view = create_initialized_view(self.bmp, '+index') | 575 | view = create_initialized_view(self.bmp, '+index') |
123 | 578 | self.assertEqual([], view.linked_bugs) | 576 | self.assertEqual([], view.linked_bugs) |
124 | 579 | 577 | ||
125 | 580 | def test_revision_end_date_active(self): | ||
126 | 581 | # An active merge proposal will have None as an end date. | ||
127 | 582 | bmp = self.factory.makeBranchMergeProposal() | ||
128 | 583 | view = create_initialized_view(bmp, '+index') | ||
129 | 584 | self.assertIs(None, view.revision_end_date) | ||
130 | 585 | |||
131 | 586 | def test_revision_end_date_merged(self): | ||
132 | 587 | # An merged proposal will have the date merged as an end date. | ||
133 | 588 | bmp = self.factory.makeBranchMergeProposal( | ||
134 | 589 | set_state=BranchMergeProposalStatus.MERGED) | ||
135 | 590 | view = create_initialized_view(bmp, '+index') | ||
136 | 591 | self.assertEqual(bmp.date_merged, view.revision_end_date) | ||
137 | 592 | |||
138 | 593 | def test_revision_end_date_rejected(self): | ||
139 | 594 | # An rejected proposal will have the date reviewed as an end date. | ||
140 | 595 | bmp = self.factory.makeBranchMergeProposal( | ||
141 | 596 | set_state=BranchMergeProposalStatus.REJECTED) | ||
142 | 597 | view = create_initialized_view(bmp, '+index') | ||
143 | 598 | self.assertEqual(bmp.date_reviewed, view.revision_end_date) | ||
144 | 599 | |||
145 | 600 | def assertRevisionGroups(self, bmp, expected_groups): | ||
146 | 601 | """Get the groups for the merge proposal and check them.""" | ||
147 | 602 | view = create_initialized_view(bmp, '+index') | ||
148 | 603 | groups = view._getRevisionsSinceReviewStart() | ||
149 | 604 | view_groups = [ | ||
150 | 605 | obj.revisions for obj in sorted( | ||
151 | 606 | groups, key=operator.attrgetter('date'))] | ||
152 | 607 | self.assertEqual(expected_groups, view_groups) | ||
153 | 608 | |||
154 | 609 | def test_getRevisionsSinceReviewStart_no_revisions(self): | ||
155 | 610 | # If there have been no revisions pushed since the start of the | ||
156 | 611 | # review, the method returns an empty list. | ||
157 | 612 | self.assertRevisionGroups(self.bmp, []) | ||
158 | 613 | |||
159 | 614 | def test_getRevisionsSinceReviewStart_groups(self): | ||
160 | 615 | # Revisions that were scanned at the same time have the same | ||
161 | 616 | # date_created. These revisions are grouped together. | ||
162 | 617 | review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC) | ||
163 | 618 | bmp = self.factory.makeBranchMergeProposal( | ||
164 | 619 | date_created=review_date) | ||
165 | 620 | login_person(bmp.registrant) | ||
166 | 621 | bmp.requestReview(review_date) | ||
167 | 622 | revision_date = review_date + timedelta(days=1) | ||
168 | 623 | revisions = [] | ||
169 | 624 | for date in range(2): | ||
170 | 625 | revisions.append( | ||
171 | 626 | add_revision_to_branch( | ||
172 | 627 | self.factory, bmp.source_branch, revision_date)) | ||
173 | 628 | revisions.append( | ||
174 | 629 | add_revision_to_branch( | ||
175 | 630 | self.factory, bmp.source_branch, revision_date)) | ||
176 | 631 | revision_date += timedelta(days=1) | ||
177 | 632 | expected_groups = [ | ||
178 | 633 | [revisions[0], revisions[1]], | ||
179 | 634 | [revisions[2], revisions[3]]] | ||
180 | 635 | self.assertRevisionGroups(bmp, expected_groups) | ||
181 | 636 | |||
182 | 637 | def test_include_superseded_comments(self): | 578 | def test_include_superseded_comments(self): |
183 | 638 | for x, time in zip(range(3), time_counter()): | 579 | for x, time in zip(range(3), time_counter()): |
184 | 639 | if x != 0: | 580 | if x != 0: |
185 | @@ -757,7 +698,6 @@ | |||
186 | 757 | 698 | ||
187 | 758 | layer = LaunchpadFunctionalLayer | 699 | layer = LaunchpadFunctionalLayer |
188 | 759 | 700 | ||
189 | 760 | |||
190 | 761 | def _makeCommentFromEmailWithAttachment(self, attachment_body): | 701 | def _makeCommentFromEmailWithAttachment(self, attachment_body): |
191 | 762 | # Make an email message with an attachment, and create a code | 702 | # Make an email message with an attachment, and create a code |
192 | 763 | # review comment from it. | 703 | # review comment from it. |
193 | 764 | 704 | ||
194 | === modified file 'lib/lp/code/configure.zcml' | |||
195 | --- lib/lp/code/configure.zcml 2010-09-20 22:16:32 +0000 | |||
196 | +++ lib/lp/code/configure.zcml 2010-09-21 15:06:00 +0000 | |||
197 | @@ -236,8 +236,10 @@ | |||
198 | 236 | votes | 236 | votes |
199 | 237 | all_comments | 237 | all_comments |
200 | 238 | related_bugs | 238 | related_bugs |
201 | 239 | revision_end_date | ||
202 | 239 | isMergable | 240 | isMergable |
203 | 240 | getComment | 241 | getComment |
204 | 242 | getRevisionsSinceReviewStart | ||
205 | 241 | getNotificationRecipients | 243 | getNotificationRecipients |
206 | 242 | getVoteReference | 244 | getVoteReference |
207 | 243 | isValidTransition | 245 | isValidTransition |
208 | 244 | 246 | ||
209 | === modified file 'lib/lp/code/interfaces/branchmergeproposal.py' | |||
210 | --- lib/lp/code/interfaces/branchmergeproposal.py 2010-08-20 20:31:18 +0000 | |||
211 | +++ lib/lp/code/interfaces/branchmergeproposal.py 2010-09-21 15:06:00 +0000 | |||
212 | @@ -290,6 +290,13 @@ | |||
213 | 290 | def getComment(id): | 290 | def getComment(id): |
214 | 291 | """Return the CodeReviewComment with the specified ID.""" | 291 | """Return the CodeReviewComment with the specified ID.""" |
215 | 292 | 292 | ||
216 | 293 | def getRevisionsSinceReviewStart(): | ||
217 | 294 | """Return all the revisions added since the review began. | ||
218 | 295 | |||
219 | 296 | Revisions are grouped by creation (i.e. push) time. | ||
220 | 297 | :return: An iterator of (date, iterator of revision data) | ||
221 | 298 | """ | ||
222 | 299 | |||
223 | 293 | def getVoteReference(id): | 300 | def getVoteReference(id): |
224 | 294 | """Return the CodeReviewVoteReference with the specified ID.""" | 301 | """Return the CodeReviewVoteReference with the specified ID.""" |
225 | 295 | 302 | ||
226 | @@ -518,8 +525,8 @@ | |||
227 | 518 | source branch. | 525 | source branch. |
228 | 519 | :param target_revision_id: The revision id that was used from the | 526 | :param target_revision_id: The revision id that was used from the |
229 | 520 | target branch. | 527 | target branch. |
232 | 521 | :param prerequisite_revision_id: The revision id that was used from the | 528 | :param prerequisite_revision_id: The revision id that was used from |
233 | 522 | prerequisite branch. | 529 | the prerequisite branch. |
234 | 523 | :param conflicts: Text describing the conflicts if any. | 530 | :param conflicts: Text describing the conflicts if any. |
235 | 524 | """ | 531 | """ |
236 | 525 | 532 | ||
237 | 526 | 533 | ||
238 | === modified file 'lib/lp/code/interfaces/revision.py' | |||
239 | --- lib/lp/code/interfaces/revision.py 2010-08-20 20:31:18 +0000 | |||
240 | +++ lib/lp/code/interfaces/revision.py 2010-09-21 15:06:00 +0000 | |||
241 | @@ -73,6 +73,9 @@ | |||
242 | 73 | :return: A `Branch` or None if an appropriate branch cannot be found. | 73 | :return: A `Branch` or None if an appropriate branch cannot be found. |
243 | 74 | """ | 74 | """ |
244 | 75 | 75 | ||
245 | 76 | def getLefthandParent(): | ||
246 | 77 | """Return lefthand parent of revision, or None if not in database.""" | ||
247 | 78 | |||
248 | 76 | 79 | ||
249 | 77 | class IRevisionAuthor(Interface): | 80 | class IRevisionAuthor(Interface): |
250 | 78 | """Committer of a Bazaar revision.""" | 81 | """Committer of a Bazaar revision.""" |
251 | 79 | 82 | ||
252 | === modified file 'lib/lp/code/model/branchmergeproposal.py' | |||
253 | --- lib/lp/code/model/branchmergeproposal.py 2010-08-20 20:31:18 +0000 | |||
254 | +++ lib/lp/code/model/branchmergeproposal.py 2010-09-21 15:06:00 +0000 | |||
255 | @@ -13,7 +13,7 @@ | |||
256 | 13 | ] | 13 | ] |
257 | 14 | 14 | ||
258 | 15 | from email.Utils import make_msgid | 15 | from email.Utils import make_msgid |
260 | 16 | 16 | from itertools import groupby | |
261 | 17 | from sqlobject import ( | 17 | from sqlobject import ( |
262 | 18 | ForeignKey, | 18 | ForeignKey, |
263 | 19 | IntCol, | 19 | IntCol, |
264 | @@ -777,6 +777,45 @@ | |||
265 | 777 | Store.of(self).flush() | 777 | Store.of(self).flush() |
266 | 778 | return self.preview_diff | 778 | return self.preview_diff |
267 | 779 | 779 | ||
268 | 780 | @property | ||
269 | 781 | def revision_end_date(self): | ||
270 | 782 | """The cutoff date for showing revisions. | ||
271 | 783 | |||
272 | 784 | If the proposal has been merged, then we stop at the merged date. If | ||
273 | 785 | it is rejected, we stop at the reviewed date. For superseded | ||
274 | 786 | proposals, it should ideally use the non-existant date_last_modified, | ||
275 | 787 | but could use the last comment date. | ||
276 | 788 | """ | ||
277 | 789 | status = self.queue_status | ||
278 | 790 | if status == BranchMergeProposalStatus.MERGED: | ||
279 | 791 | return self.date_merged | ||
280 | 792 | if status == BranchMergeProposalStatus.REJECTED: | ||
281 | 793 | return self.date_reviewed | ||
282 | 794 | # Otherwise return None representing an open end date. | ||
283 | 795 | return None | ||
284 | 796 | |||
285 | 797 | def _getNewerRevisions(self): | ||
286 | 798 | start_date = self.date_review_requested | ||
287 | 799 | if start_date is None: | ||
288 | 800 | start_date = self.date_created | ||
289 | 801 | return self.source_branch.getMainlineBranchRevisions( | ||
290 | 802 | start_date, self.revision_end_date, oldest_first=True) | ||
291 | 803 | |||
292 | 804 | def getRevisionsSinceReviewStart(self): | ||
293 | 805 | """Get the grouped revisions since the review started.""" | ||
294 | 806 | resultset = self._getNewerRevisions() | ||
295 | 807 | # Work out the start of the review. | ||
296 | 808 | branch_revisions = ( | ||
297 | 809 | branch_revision for branch_revision, revision, revision_author | ||
298 | 810 | in resultset) | ||
299 | 811 | # Now group by date created. | ||
300 | 812 | gby = groupby(branch_revisions, lambda r: r.revision.date_created) | ||
301 | 813 | # Use a generator expression to wrap the custom iterator so it doesn't | ||
302 | 814 | # get security-proxied. | ||
303 | 815 | return ( | ||
304 | 816 | (date, (revision for revision in revisions)) | ||
305 | 817 | for date, revisions in gby) | ||
306 | 818 | |||
307 | 780 | 819 | ||
308 | 781 | class BranchMergeProposalGetter: | 820 | class BranchMergeProposalGetter: |
309 | 782 | """See `IBranchMergeProposalGetter`.""" | 821 | """See `IBranchMergeProposalGetter`.""" |
310 | 783 | 822 | ||
311 | === modified file 'lib/lp/code/model/directbranchcommit.py' | |||
312 | --- lib/lp/code/model/directbranchcommit.py 2010-08-20 20:31:18 +0000 | |||
313 | +++ lib/lp/code/model/directbranchcommit.py 2010-09-21 15:06:00 +0000 | |||
314 | @@ -55,7 +55,8 @@ | |||
315 | 55 | is_locked = False | 55 | is_locked = False |
316 | 56 | commit_builder = None | 56 | commit_builder = None |
317 | 57 | 57 | ||
319 | 58 | def __init__(self, db_branch, committer=None, no_race_check=False): | 58 | def __init__(self, db_branch, committer=None, no_race_check=False, |
320 | 59 | merge_parents=None): | ||
321 | 59 | """Create context for direct commit to branch. | 60 | """Create context for direct commit to branch. |
322 | 60 | 61 | ||
323 | 61 | Before constructing a `DirectBranchCommit`, set up a server that | 62 | Before constructing a `DirectBranchCommit`, set up a server that |
324 | @@ -107,6 +108,7 @@ | |||
325 | 107 | raise | 108 | raise |
326 | 108 | 109 | ||
327 | 109 | self.files = set() | 110 | self.files = set() |
328 | 111 | self.merge_parents = merge_parents | ||
329 | 110 | 112 | ||
330 | 111 | def _getDir(self, path): | 113 | def _getDir(self, path): |
331 | 112 | """Get trans_id for directory "path." Create if necessary.""" | 114 | """Get trans_id for directory "path." Create if necessary.""" |
332 | @@ -200,7 +202,8 @@ | |||
333 | 200 | # required to generate the revision-id. | 202 | # required to generate the revision-id. |
334 | 201 | with override_environ(BZR_EMAIL=committer_id): | 203 | with override_environ(BZR_EMAIL=committer_id): |
335 | 202 | new_rev_id = self.transform_preview.commit( | 204 | new_rev_id = self.transform_preview.commit( |
337 | 203 | self.bzrbranch, commit_message, committer=committer_id) | 205 | self.bzrbranch, commit_message, self.merge_parents, |
338 | 206 | committer=committer_id) | ||
339 | 204 | IMasterObject(self.db_branch).branchChanged( | 207 | IMasterObject(self.db_branch).branchChanged( |
340 | 205 | get_stacked_on_url(self.bzrbranch), new_rev_id, | 208 | get_stacked_on_url(self.bzrbranch), new_rev_id, |
341 | 206 | self.db_branch.control_format, self.db_branch.branch_format, | 209 | self.db_branch.control_format, self.db_branch.branch_format, |
342 | 207 | 210 | ||
343 | === modified file 'lib/lp/code/model/revision.py' | |||
344 | --- lib/lp/code/model/revision.py 2010-08-27 02:11:36 +0000 | |||
345 | +++ lib/lp/code/model/revision.py 2010-09-21 15:06:00 +0000 | |||
346 | @@ -18,6 +18,7 @@ | |||
347 | 18 | ) | 18 | ) |
348 | 19 | import email | 19 | import email |
349 | 20 | 20 | ||
350 | 21 | from bzrlib.revision import NULL_REVISION | ||
351 | 21 | import pytz | 22 | import pytz |
352 | 22 | from sqlobject import ( | 23 | from sqlobject import ( |
353 | 23 | BoolCol, | 24 | BoolCol, |
354 | @@ -39,7 +40,6 @@ | |||
355 | 39 | ) | 40 | ) |
356 | 40 | from storm.locals import ( | 41 | from storm.locals import ( |
357 | 41 | Bool, | 42 | Bool, |
358 | 42 | DateTime, | ||
359 | 43 | Int, | 43 | Int, |
360 | 44 | Min, | 44 | Min, |
361 | 45 | Reference, | 45 | Reference, |
362 | @@ -118,6 +118,13 @@ | |||
363 | 118 | """ | 118 | """ |
364 | 119 | return [parent.parent_id for parent in self.parents] | 119 | return [parent.parent_id for parent in self.parents] |
365 | 120 | 120 | ||
366 | 121 | def getLefthandParent(self): | ||
367 | 122 | if len(self.parent_ids) == 0: | ||
368 | 123 | parent_id = NULL_REVISION | ||
369 | 124 | else: | ||
370 | 125 | parent_id = self.parent_ids[0] | ||
371 | 126 | return RevisionSet().getByRevisionId(parent_id) | ||
372 | 127 | |||
373 | 121 | def getProperties(self): | 128 | def getProperties(self): |
374 | 122 | """See `IRevision`.""" | 129 | """See `IRevision`.""" |
375 | 123 | return dict((prop.name, prop.value) for prop in self.properties) | 130 | return dict((prop.name, prop.value) for prop in self.properties) |
376 | 124 | 131 | ||
377 | === modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py' | |||
378 | --- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-08-20 20:31:18 +0000 | |||
379 | +++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-09-21 15:06:00 +0000 | |||
380 | @@ -7,7 +7,7 @@ | |||
381 | 7 | 7 | ||
382 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
383 | 9 | 9 | ||
385 | 10 | from datetime import datetime | 10 | from datetime import datetime, timedelta |
386 | 11 | from difflib import unified_diff | 11 | from difflib import unified_diff |
387 | 12 | from unittest import ( | 12 | from unittest import ( |
388 | 13 | TestCase, | 13 | TestCase, |
389 | @@ -72,6 +72,7 @@ | |||
390 | 72 | MergeProposalCreatedJob, | 72 | MergeProposalCreatedJob, |
391 | 73 | UpdatePreviewDiffJob, | 73 | UpdatePreviewDiffJob, |
392 | 74 | ) | 74 | ) |
393 | 75 | from lp.code.tests.helpers import add_revision_to_branch | ||
394 | 75 | from lp.registry.interfaces.person import IPersonSet | 76 | from lp.registry.interfaces.person import IPersonSet |
395 | 76 | from lp.registry.interfaces.product import IProductSet | 77 | from lp.registry.interfaces.product import IProductSet |
396 | 77 | from lp.testing import ( | 78 | from lp.testing import ( |
397 | @@ -108,7 +109,8 @@ | |||
398 | 108 | self.assertTrue(url.startswith(source_branch_url)) | 109 | self.assertTrue(url.startswith(source_branch_url)) |
399 | 109 | 110 | ||
400 | 110 | def test_BranchMergeProposal_canonical_url_rest(self): | 111 | def test_BranchMergeProposal_canonical_url_rest(self): |
402 | 111 | # The rest of the URL for a merge proposal is +merge followed by the db id. | 112 | # The rest of the URL for a merge proposal is +merge followed by the |
403 | 113 | # db id. | ||
404 | 112 | bmp = self.factory.makeBranchMergeProposal() | 114 | bmp = self.factory.makeBranchMergeProposal() |
405 | 113 | url = canonical_url(bmp) | 115 | url = canonical_url(bmp) |
406 | 114 | source_branch_url = canonical_url(bmp.source_branch) | 116 | source_branch_url = canonical_url(bmp.source_branch) |
407 | @@ -238,7 +240,6 @@ | |||
408 | 238 | self._attemptTransition, | 240 | self._attemptTransition, |
409 | 239 | proposal, to_state) | 241 | proposal, to_state) |
410 | 240 | 242 | ||
411 | 241 | |||
412 | 242 | def assertGoodDupeTransition(self, from_state, to_state): | 243 | def assertGoodDupeTransition(self, from_state, to_state): |
413 | 243 | """Trying to go from `from_state` to `to_state` succeeds.""" | 244 | """Trying to go from `from_state` to `to_state` succeeds.""" |
414 | 244 | proposal = self.prepareDupeTransition(from_state) | 245 | proposal = self.prepareDupeTransition(from_state) |
415 | @@ -1049,6 +1050,7 @@ | |||
416 | 1049 | else: | 1050 | else: |
417 | 1050 | self.assertEqual([mp], list(active)) | 1051 | self.assertEqual([mp], list(active)) |
418 | 1051 | 1052 | ||
419 | 1053 | |||
420 | 1052 | class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory): | 1054 | class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory): |
421 | 1053 | """Test the getProposalsForContext method.""" | 1055 | """Test the getProposalsForContext method.""" |
422 | 1054 | 1056 | ||
423 | @@ -1118,7 +1120,6 @@ | |||
424 | 1118 | beaver, [BranchMergeProposalStatus.REJECTED], beaver) | 1120 | beaver, [BranchMergeProposalStatus.REJECTED], beaver) |
425 | 1119 | self.assertEqual(beave_proposals.count(), 1) | 1121 | self.assertEqual(beave_proposals.count(), 1) |
426 | 1120 | 1122 | ||
427 | 1121 | |||
428 | 1122 | def test_created_proposal_default_status(self): | 1123 | def test_created_proposal_default_status(self): |
429 | 1123 | # When we create a merge proposal using the helper method, the default | 1124 | # When we create a merge proposal using the helper method, the default |
430 | 1124 | # status of the proposal is work in progress. | 1125 | # status of the proposal is work in progress. |
431 | @@ -1799,5 +1800,67 @@ | |||
432 | 1799 | self.assertIs(None, bmp.next_preview_diff_job) | 1800 | self.assertIs(None, bmp.next_preview_diff_job) |
433 | 1800 | 1801 | ||
434 | 1801 | 1802 | ||
435 | 1803 | class TestRevisionEndDate(TestCaseWithFactory): | ||
436 | 1804 | |||
437 | 1805 | layer = DatabaseFunctionalLayer | ||
438 | 1806 | |||
439 | 1807 | def test_revision_end_date_active(self): | ||
440 | 1808 | # An active merge proposal will have None as an end date. | ||
441 | 1809 | bmp = self.factory.makeBranchMergeProposal() | ||
442 | 1810 | self.assertIs(None, bmp.revision_end_date) | ||
443 | 1811 | |||
444 | 1812 | def test_revision_end_date_merged(self): | ||
445 | 1813 | # An merged proposal will have the date merged as an end date. | ||
446 | 1814 | bmp = self.factory.makeBranchMergeProposal( | ||
447 | 1815 | set_state=BranchMergeProposalStatus.MERGED) | ||
448 | 1816 | self.assertEqual(bmp.date_merged, bmp.revision_end_date) | ||
449 | 1817 | |||
450 | 1818 | def test_revision_end_date_rejected(self): | ||
451 | 1819 | # An rejected proposal will have the date reviewed as an end date. | ||
452 | 1820 | bmp = self.factory.makeBranchMergeProposal( | ||
453 | 1821 | set_state=BranchMergeProposalStatus.REJECTED) | ||
454 | 1822 | self.assertEqual(bmp.date_reviewed, bmp.revision_end_date) | ||
455 | 1823 | |||
456 | 1824 | |||
457 | 1825 | class TestGetRevisionsSinceReviewStart(TestCaseWithFactory): | ||
458 | 1826 | |||
459 | 1827 | layer = DatabaseFunctionalLayer | ||
460 | 1828 | |||
461 | 1829 | def assertRevisionGroups(self, bmp, expected_groups): | ||
462 | 1830 | """Get the groups for the merge proposal and check them.""" | ||
463 | 1831 | groups = bmp.getRevisionsSinceReviewStart() | ||
464 | 1832 | revision_groups = [list(revisions) for date, revisions in groups] | ||
465 | 1833 | self.assertEqual(expected_groups, revision_groups) | ||
466 | 1834 | |||
467 | 1835 | def test_getRevisionsSinceReviewStart_no_revisions(self): | ||
468 | 1836 | # If there have been no revisions pushed since the start of the | ||
469 | 1837 | # review, the method returns an empty list. | ||
470 | 1838 | bmp = self.factory.makeBranchMergeProposal() | ||
471 | 1839 | self.assertRevisionGroups(bmp, []) | ||
472 | 1840 | |||
473 | 1841 | def test_getRevisionsSinceReviewStart_groups(self): | ||
474 | 1842 | # Revisions that were scanned at the same time have the same | ||
475 | 1843 | # date_created. These revisions are grouped together. | ||
476 | 1844 | review_date = datetime(2009, 9, 10, tzinfo=UTC) | ||
477 | 1845 | bmp = self.factory.makeBranchMergeProposal( | ||
478 | 1846 | date_created=review_date) | ||
479 | 1847 | login_person(bmp.registrant) | ||
480 | 1848 | bmp.requestReview(review_date) | ||
481 | 1849 | revision_date = review_date + timedelta(days=1) | ||
482 | 1850 | revisions = [] | ||
483 | 1851 | for date in range(2): | ||
484 | 1852 | revisions.append( | ||
485 | 1853 | add_revision_to_branch( | ||
486 | 1854 | self.factory, bmp.source_branch, revision_date)) | ||
487 | 1855 | revisions.append( | ||
488 | 1856 | add_revision_to_branch( | ||
489 | 1857 | self.factory, bmp.source_branch, revision_date)) | ||
490 | 1858 | revision_date += timedelta(days=1) | ||
491 | 1859 | expected_groups = [ | ||
492 | 1860 | [revisions[0], revisions[1]], | ||
493 | 1861 | [revisions[2], revisions[3]]] | ||
494 | 1862 | self.assertRevisionGroups(bmp, expected_groups) | ||
495 | 1863 | |||
496 | 1864 | |||
497 | 1802 | def test_suite(): | 1865 | def test_suite(): |
498 | 1803 | return TestLoader().loadTestsFromName(__name__) | 1866 | return TestLoader().loadTestsFromName(__name__) |
499 | 1804 | 1867 | ||
500 | === modified file 'lib/lp/code/model/tests/test_diff.py' | |||
501 | --- lib/lp/code/model/tests/test_diff.py 2010-08-20 20:31:18 +0000 | |||
502 | +++ lib/lp/code/model/tests/test_diff.py 2010-09-21 15:06:00 +0000 | |||
503 | @@ -57,13 +57,14 @@ | |||
504 | 57 | class DiffTestCase(TestCaseWithFactory): | 57 | class DiffTestCase(TestCaseWithFactory): |
505 | 58 | 58 | ||
506 | 59 | @staticmethod | 59 | @staticmethod |
508 | 60 | def commitFile(branch, path, contents): | 60 | def commitFile(branch, path, contents, merge_parents=None): |
509 | 61 | """Create a commit that updates a file to specified contents. | 61 | """Create a commit that updates a file to specified contents. |
510 | 62 | 62 | ||
511 | 63 | This will create or modify the file, as needed. | 63 | This will create or modify the file, as needed. |
512 | 64 | """ | 64 | """ |
513 | 65 | committer = DirectBranchCommit( | 65 | committer = DirectBranchCommit( |
515 | 66 | removeSecurityProxy(branch), no_race_check=True) | 66 | removeSecurityProxy(branch), no_race_check=True, |
516 | 67 | merge_parents=merge_parents) | ||
517 | 67 | committer.writeFile(path, contents) | 68 | committer.writeFile(path, contents) |
518 | 68 | try: | 69 | try: |
519 | 69 | return committer.commit('committing') | 70 | return committer.commit('committing') |
520 | @@ -122,7 +123,6 @@ | |||
521 | 122 | prerequisite) | 123 | prerequisite) |
522 | 123 | 124 | ||
523 | 124 | 125 | ||
524 | 125 | |||
525 | 126 | class TestDiff(DiffTestCase): | 126 | class TestDiff(DiffTestCase): |
526 | 127 | 127 | ||
527 | 128 | layer = LaunchpadFunctionalLayer | 128 | layer = LaunchpadFunctionalLayer |
528 | @@ -186,19 +186,19 @@ | |||
529 | 186 | self.checkExampleMerge(diff.text) | 186 | self.checkExampleMerge(diff.text) |
530 | 187 | 187 | ||
531 | 188 | diff_bytes = ( | 188 | diff_bytes = ( |
534 | 189 | "--- bar 2009-08-26 15:53:34.000000000 -0400\n" | 189 | "--- bar\t2009-08-26 15:53:34.000000000 -0400\n" |
535 | 190 | "+++ bar 1969-12-31 19:00:00.000000000 -0500\n" | 190 | "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n" |
536 | 191 | "@@ -1,3 +0,0 @@\n" | 191 | "@@ -1,3 +0,0 @@\n" |
537 | 192 | "-a\n" | 192 | "-a\n" |
538 | 193 | "-b\n" | 193 | "-b\n" |
539 | 194 | "-c\n" | 194 | "-c\n" |
542 | 195 | "--- baz 1969-12-31 19:00:00.000000000 -0500\n" | 195 | "--- baz\t1969-12-31 19:00:00.000000000 -0500\n" |
543 | 196 | "+++ baz 2009-08-26 15:53:57.000000000 -0400\n" | 196 | "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n" |
544 | 197 | "@@ -0,0 +1,2 @@\n" | 197 | "@@ -0,0 +1,2 @@\n" |
545 | 198 | "+a\n" | 198 | "+a\n" |
546 | 199 | "+b\n" | 199 | "+b\n" |
549 | 200 | "--- foo 2009-08-26 15:53:23.000000000 -0400\n" | 200 | "--- foo\t2009-08-26 15:53:23.000000000 -0400\n" |
550 | 201 | "+++ foo 2009-08-26 15:56:43.000000000 -0400\n" | 201 | "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n" |
551 | 202 | "@@ -1,3 +1,4 @@\n" | 202 | "@@ -1,3 +1,4 @@\n" |
552 | 203 | " a\n" | 203 | " a\n" |
553 | 204 | "-b\n" | 204 | "-b\n" |
554 | @@ -207,19 +207,19 @@ | |||
555 | 207 | "+e\n") | 207 | "+e\n") |
556 | 208 | 208 | ||
557 | 209 | diff_bytes_2 = ( | 209 | diff_bytes_2 = ( |
560 | 210 | "--- bar 2009-08-26 15:53:34.000000000 -0400\n" | 210 | "--- bar\t2009-08-26 15:53:34.000000000 -0400\n" |
561 | 211 | "+++ bar 1969-12-31 19:00:00.000000000 -0500\n" | 211 | "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n" |
562 | 212 | "@@ -1,3 +0,0 @@\n" | 212 | "@@ -1,3 +0,0 @@\n" |
563 | 213 | "-a\n" | 213 | "-a\n" |
564 | 214 | "-b\n" | 214 | "-b\n" |
565 | 215 | "-c\n" | 215 | "-c\n" |
568 | 216 | "--- baz 1969-12-31 19:00:00.000000000 -0500\n" | 216 | "--- baz\t1969-12-31 19:00:00.000000000 -0500\n" |
569 | 217 | "+++ baz 2009-08-26 15:53:57.000000000 -0400\n" | 217 | "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n" |
570 | 218 | "@@ -0,0 +1,2 @@\n" | 218 | "@@ -0,0 +1,2 @@\n" |
571 | 219 | "+a\n" | 219 | "+a\n" |
572 | 220 | "+b\n" | 220 | "+b\n" |
575 | 221 | "--- foo 2009-08-26 15:53:23.000000000 -0400\n" | 221 | "--- foo\t2009-08-26 15:53:23.000000000 -0400\n" |
576 | 222 | "+++ foo 2009-08-26 15:56:43.000000000 -0400\n" | 222 | "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n" |
577 | 223 | "@@ -1,3 +1,5 @@\n" | 223 | "@@ -1,3 +1,5 @@\n" |
578 | 224 | " a\n" | 224 | " a\n" |
579 | 225 | "-b\n" | 225 | "-b\n" |
580 | @@ -467,7 +467,6 @@ | |||
581 | 467 | self.assertEqual('', diff.conflicts) | 467 | self.assertEqual('', diff.conflicts) |
582 | 468 | self.assertFalse(diff.has_conflicts) | 468 | self.assertFalse(diff.has_conflicts) |
583 | 469 | 469 | ||
584 | 470 | |||
585 | 471 | def test_fromBranchMergeProposal(self): | 470 | def test_fromBranchMergeProposal(self): |
586 | 472 | # Correctly generates a PreviewDiff from a BranchMergeProposal. | 471 | # Correctly generates a PreviewDiff from a BranchMergeProposal. |
587 | 473 | bmp, source_rev_id, target_rev_id = self.createExampleMerge() | 472 | bmp, source_rev_id, target_rev_id = self.createExampleMerge() |
588 | 474 | 473 | ||
589 | === modified file 'lib/lp/code/tests/helpers.py' | |||
590 | --- lib/lp/code/tests/helpers.py 2010-08-31 00:16:41 +0000 | |||
591 | +++ lib/lp/code/tests/helpers.py 2010-09-21 15:06:00 +0000 | |||
592 | @@ -64,9 +64,14 @@ | |||
593 | 64 | """ | 64 | """ |
594 | 65 | if date_created is None: | 65 | if date_created is None: |
595 | 66 | date_created = revision_date | 66 | date_created = revision_date |
596 | 67 | parent = branch.revision_history.last() | ||
597 | 68 | if parent is None: | ||
598 | 69 | parent_ids = [] | ||
599 | 70 | else: | ||
600 | 71 | parent_ids = [parent.revision.revision_id] | ||
601 | 67 | revision = factory.makeRevision( | 72 | revision = factory.makeRevision( |
602 | 68 | revision_date=revision_date, date_created=date_created, | 73 | revision_date=revision_date, date_created=date_created, |
604 | 69 | log_body=commit_msg) | 74 | log_body=commit_msg, parent_ids=parent_ids) |
605 | 70 | if mainline: | 75 | if mainline: |
606 | 71 | sequence = branch.revision_count + 1 | 76 | sequence = branch.revision_count + 1 |
607 | 72 | branch_revision = branch.createBranchRevision(sequence, revision) | 77 | branch_revision = branch.createBranchRevision(sequence, revision) |
608 | @@ -112,7 +117,7 @@ | |||
609 | 112 | preview.remvoed_lines_count = 13 | 117 | preview.remvoed_lines_count = 13 |
610 | 113 | preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)} | 118 | preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)} |
611 | 114 | return { | 119 | return { |
613 | 115 | 'eric': eric, 'fooix': fooix, 'trunk':trunk, 'feature': feature, | 120 | 'eric': eric, 'fooix': fooix, 'trunk': trunk, 'feature': feature, |
614 | 116 | 'proposed': proposed, 'fred': fred} | 121 | 'proposed': proposed, 'fred': fred} |
615 | 117 | 122 | ||
616 | 118 | 123 | ||
617 | 119 | 124 | ||
618 | === modified file 'lib/lp/code/tests/test_directbranchcommit.py' | |||
619 | --- lib/lp/code/tests/test_directbranchcommit.py 2010-08-20 20:31:18 +0000 | |||
620 | +++ lib/lp/code/tests/test_directbranchcommit.py 2010-09-21 15:06:00 +0000 | |||
621 | @@ -99,6 +99,24 @@ | |||
622 | 99 | branch_revision_id = self.committer.bzrbranch.last_revision() | 99 | branch_revision_id = self.committer.bzrbranch.last_revision() |
623 | 100 | self.assertEqual(branch_revision_id, revision_id) | 100 | self.assertEqual(branch_revision_id, revision_id) |
624 | 101 | 101 | ||
625 | 102 | def test_commit_uses_merge_parents(self): | ||
626 | 103 | # DirectBranchCommit.commit returns uses merge parents | ||
627 | 104 | self._tearDownCommitter() | ||
628 | 105 | # Merge parents cannot be specified for initial commit, so do an | ||
629 | 106 | # empty commit. | ||
630 | 107 | self.tree.commit('foo', committer='foo@bar', rev_id='foo') | ||
631 | 108 | committer = DirectBranchCommit( | ||
632 | 109 | self.db_branch, merge_parents=['parent-1', 'parent-2']) | ||
633 | 110 | committer.last_scanned_id = ( | ||
634 | 111 | committer.bzrbranch.last_revision()) | ||
635 | 112 | committer.writeFile('file.txt', 'contents') | ||
636 | 113 | revision_id = committer.commit('') | ||
637 | 114 | branch_revision_id = committer.bzrbranch.last_revision() | ||
638 | 115 | branch_revision = committer.bzrbranch.repository.get_revision( | ||
639 | 116 | branch_revision_id) | ||
640 | 117 | self.assertEqual( | ||
641 | 118 | ['parent-1', 'parent-2'], branch_revision.parent_ids[1:]) | ||
642 | 119 | |||
643 | 102 | def test_DirectBranchCommit_aborts_cleanly(self): | 120 | def test_DirectBranchCommit_aborts_cleanly(self): |
644 | 103 | # If a DirectBranchCommit is not committed, its changes do not | 121 | # If a DirectBranchCommit is not committed, its changes do not |
645 | 104 | # go into the branch. | 122 | # go into the branch. |
646 | 105 | 123 | ||
647 | === modified file 'lib/lp/codehosting/bzrutils.py' | |||
648 | --- lib/lp/codehosting/bzrutils.py 2010-08-20 20:31:18 +0000 | |||
649 | +++ lib/lp/codehosting/bzrutils.py 2010-09-21 15:06:00 +0000 | |||
650 | @@ -18,11 +18,13 @@ | |||
651 | 18 | 'identical_formats', | 18 | 'identical_formats', |
652 | 19 | 'install_oops_handler', | 19 | 'install_oops_handler', |
653 | 20 | 'is_branch_stackable', | 20 | 'is_branch_stackable', |
654 | 21 | 'read_locked', | ||
655 | 21 | 'remove_exception_logging_hook', | 22 | 'remove_exception_logging_hook', |
656 | 22 | 'safe_open', | 23 | 'safe_open', |
657 | 23 | 'UnsafeUrlSeen', | 24 | 'UnsafeUrlSeen', |
658 | 24 | ] | 25 | ] |
659 | 25 | 26 | ||
660 | 27 | from contextlib import contextmanager | ||
661 | 26 | import os | 28 | import os |
662 | 27 | import sys | 29 | import sys |
663 | 28 | import threading | 30 | import threading |
664 | @@ -363,3 +365,12 @@ | |||
665 | 363 | return branch.get_stacked_on_url() | 365 | return branch.get_stacked_on_url() |
666 | 364 | except (NotStacked, UnstackableBranchFormat): | 366 | except (NotStacked, UnstackableBranchFormat): |
667 | 365 | return None | 367 | return None |
668 | 368 | |||
669 | 369 | |||
670 | 370 | @contextmanager | ||
671 | 371 | def read_locked(branch): | ||
672 | 372 | branch.lock_read() | ||
673 | 373 | try: | ||
674 | 374 | yield | ||
675 | 375 | finally: | ||
676 | 376 | branch.unlock() |