Merge lp:~cjwatson/launchpad/git-default-branch into lp:launchpad
- git-default-branch
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 17568 |
Proposed branch: | lp:~cjwatson/launchpad/git-default-branch |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/git-detect-merges |
Diff against target: |
436 lines (+196/-6) 11 files modified
lib/lp/code/browser/gitrepository.py (+14/-1) lib/lp/code/browser/tests/test_gitrepository.py (+48/-0) lib/lp/code/configure.zcml (+4/-2) lib/lp/code/errors.py (+20/-1) lib/lp/code/interfaces/githosting.py (+14/-0) lib/lp/code/interfaces/gitrepository.py (+15/-1) lib/lp/code/model/githosting.py (+19/-0) lib/lp/code/model/gitjob.py (+7/-0) lib/lp/code/model/gitrepository.py (+19/-0) lib/lp/code/model/tests/test_gitjob.py (+6/-1) lib/lp/code/model/tests/test_gitrepository.py (+30/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-default-branch |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+261892@code.launchpad.net |
Commit message
Allow getting and setting the default branch of a Git repository.
Description of the change
Allow getting and setting the default branch of a Git repository.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/browser/gitrepository.py' | |||
2 | --- lib/lp/code/browser/gitrepository.py 2015-06-12 08:04:42 +0000 | |||
3 | +++ lib/lp/code/browser/gitrepository.py 2015-06-15 10:02:42 +0000 | |||
4 | @@ -21,7 +21,10 @@ | |||
5 | 21 | 21 | ||
6 | 22 | from lazr.lifecycle.event import ObjectModifiedEvent | 22 | from lazr.lifecycle.event import ObjectModifiedEvent |
7 | 23 | from lazr.lifecycle.snapshot import Snapshot | 23 | from lazr.lifecycle.snapshot import Snapshot |
9 | 24 | from lazr.restful.interface import copy_field | 24 | from lazr.restful.interface import ( |
10 | 25 | copy_field, | ||
11 | 26 | use_template, | ||
12 | 27 | ) | ||
13 | 25 | from storm.expr import Desc | 28 | from storm.expr import Desc |
14 | 26 | from zope.event import notify | 29 | from zope.event import notify |
15 | 27 | from zope.interface import ( | 30 | from zope.interface import ( |
16 | @@ -266,6 +269,7 @@ | |||
17 | 266 | This is necessary to make various fields editable that are not | 269 | This is necessary to make various fields editable that are not |
18 | 267 | normally editable through the interface. | 270 | normally editable through the interface. |
19 | 268 | """ | 271 | """ |
20 | 272 | use_template(IGitRepository, include=["default_branch"]) | ||
21 | 269 | information_type = copy_field( | 273 | information_type = copy_field( |
22 | 270 | IGitRepository["information_type"], readonly=False, | 274 | IGitRepository["information_type"], readonly=False, |
23 | 271 | vocabulary=InformationTypeVocabulary(types=info_types)) | 275 | vocabulary=InformationTypeVocabulary(types=info_types)) |
24 | @@ -378,6 +382,7 @@ | |||
25 | 378 | "owner", | 382 | "owner", |
26 | 379 | "name", | 383 | "name", |
27 | 380 | "information_type", | 384 | "information_type", |
28 | 385 | "default_branch", | ||
29 | 381 | ] | 386 | ] |
30 | 382 | 387 | ||
31 | 383 | custom_widget("information_type", LaunchpadRadioWidgetWithDescription) | 388 | custom_widget("information_type", LaunchpadRadioWidgetWithDescription) |
32 | @@ -416,6 +421,14 @@ | |||
33 | 416 | (owner.displayname, target.displayname)) | 421 | (owner.displayname, target.displayname)) |
34 | 417 | except GitRepositoryExists as e: | 422 | except GitRepositoryExists as e: |
35 | 418 | self._setRepositoryExists(e.existing_repository) | 423 | self._setRepositoryExists(e.existing_repository) |
36 | 424 | if "default_branch" in data: | ||
37 | 425 | default_branch = data["default_branch"] | ||
38 | 426 | if (default_branch is not None and | ||
39 | 427 | self.context.getRefByPath(default_branch) is None): | ||
40 | 428 | self.setFieldError( | ||
41 | 429 | "default_branch", | ||
42 | 430 | "This repository does not contain a reference named " | ||
43 | 431 | "'%s'." % default_branch) | ||
44 | 419 | 432 | ||
45 | 420 | 433 | ||
46 | 421 | class GitRepositoryDeletionView(LaunchpadFormView): | 434 | class GitRepositoryDeletionView(LaunchpadFormView): |
47 | 422 | 435 | ||
48 | === modified file 'lib/lp/code/browser/tests/test_gitrepository.py' | |||
49 | --- lib/lp/code/browser/tests/test_gitrepository.py 2015-06-05 13:10:18 +0000 | |||
50 | +++ lib/lp/code/browser/tests/test_gitrepository.py 2015-06-15 10:02:42 +0000 | |||
51 | @@ -15,6 +15,7 @@ | |||
52 | 15 | DocTestMatches, | 15 | DocTestMatches, |
53 | 16 | Equals, | 16 | Equals, |
54 | 17 | ) | 17 | ) |
55 | 18 | import transaction | ||
56 | 18 | from zope.component import getUtility | 19 | from zope.component import getUtility |
57 | 19 | from zope.publisher.interfaces import NotFound | 20 | from zope.publisher.interfaces import NotFound |
58 | 20 | from zope.security.proxy import removeSecurityProxy | 21 | from zope.security.proxy import removeSecurityProxy |
59 | @@ -22,6 +23,7 @@ | |||
60 | 22 | from lp.app.enums import InformationType | 23 | from lp.app.enums import InformationType |
61 | 23 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | 24 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
62 | 24 | from lp.app.interfaces.services import IService | 25 | from lp.app.interfaces.services import IService |
63 | 26 | from lp.code.interfaces.githosting import IGitHostingClient | ||
64 | 25 | from lp.code.interfaces.revision import IRevisionSet | 27 | from lp.code.interfaces.revision import IRevisionSet |
65 | 26 | from lp.registry.enums import BranchSharingPolicy | 28 | from lp.registry.enums import BranchSharingPolicy |
66 | 27 | from lp.registry.interfaces.person import PersonVisibility | 29 | from lp.registry.interfaces.person import PersonVisibility |
67 | @@ -37,6 +39,8 @@ | |||
68 | 37 | record_two_runs, | 39 | record_two_runs, |
69 | 38 | TestCaseWithFactory, | 40 | TestCaseWithFactory, |
70 | 39 | ) | 41 | ) |
71 | 42 | from lp.testing.fakemethod import FakeMethod | ||
72 | 43 | from lp.testing.fixture import ZopeUtilityFixture | ||
73 | 40 | from lp.testing.layers import DatabaseFunctionalLayer | 44 | from lp.testing.layers import DatabaseFunctionalLayer |
74 | 41 | from lp.testing.matchers import HasQueryCount | 45 | from lp.testing.matchers import HasQueryCount |
75 | 42 | from lp.testing.pages import ( | 46 | from lp.testing.pages import ( |
76 | @@ -343,6 +347,50 @@ | |||
77 | 343 | self.assertEqual( | 347 | self.assertEqual( |
78 | 344 | repository.information_type, InformationType.PUBLICSECURITY) | 348 | repository.information_type, InformationType.PUBLICSECURITY) |
79 | 345 | 349 | ||
80 | 350 | def test_change_default_branch(self): | ||
81 | 351 | # An authorised user can change the default branch to one that | ||
82 | 352 | # exists. They may omit "refs/heads/". | ||
83 | 353 | hosting_client = FakeMethod() | ||
84 | 354 | hosting_client.setProperties = FakeMethod() | ||
85 | 355 | self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient)) | ||
86 | 356 | person = self.factory.makePerson() | ||
87 | 357 | repository = self.factory.makeGitRepository(owner=person) | ||
88 | 358 | master, new = self.factory.makeGitRefs( | ||
89 | 359 | repository=repository, | ||
90 | 360 | paths=[u"refs/heads/master", u"refs/heads/new"]) | ||
91 | 361 | removeSecurityProxy(repository)._default_branch = u"refs/heads/master" | ||
92 | 362 | browser = self.getUserBrowser( | ||
93 | 363 | canonical_url(repository) + "/+edit", user=person) | ||
94 | 364 | browser.getControl(name="field.default_branch").value = u"new" | ||
95 | 365 | browser.getControl("Change Git Repository").click() | ||
96 | 366 | with person_logged_in(person): | ||
97 | 367 | self.assertEqual( | ||
98 | 368 | [((repository.getInternalPath(),), | ||
99 | 369 | {u"default_branch": u"refs/heads/new"})], | ||
100 | 370 | hosting_client.setProperties.calls) | ||
101 | 371 | self.assertEqual(u"refs/heads/new", repository.default_branch) | ||
102 | 372 | |||
103 | 373 | def test_change_default_branch_nonexistent(self): | ||
104 | 374 | # Trying to change the default branch to one that doesn't exist | ||
105 | 375 | # displays an error. | ||
106 | 376 | person = self.factory.makePerson() | ||
107 | 377 | repository = self.factory.makeGitRepository(owner=person) | ||
108 | 378 | [master] = self.factory.makeGitRefs( | ||
109 | 379 | repository=repository, paths=[u"refs/heads/master"]) | ||
110 | 380 | removeSecurityProxy(repository)._default_branch = u"refs/heads/master" | ||
111 | 381 | form = { | ||
112 | 382 | "field.default_branch": "refs/heads/new", | ||
113 | 383 | "field.actions.change": "Change Git Repository", | ||
114 | 384 | } | ||
115 | 385 | transaction.commit() | ||
116 | 386 | with person_logged_in(person): | ||
117 | 387 | view = create_initialized_view(repository, name="+edit", form=form) | ||
118 | 388 | self.assertEqual( | ||
119 | 389 | ["This repository does not contain a reference named " | ||
120 | 390 | "'refs/heads/new'."], | ||
121 | 391 | view.errors) | ||
122 | 392 | self.assertEqual(u"refs/heads/master", repository.default_branch) | ||
123 | 393 | |||
124 | 346 | 394 | ||
125 | 347 | class TestGitRepositoryEditViewInformationTypes(TestCaseWithFactory): | 395 | class TestGitRepositoryEditViewInformationTypes(TestCaseWithFactory): |
126 | 348 | """Tests for GitRepositoryEditView.getInformationTypesToShow.""" | 396 | """Tests for GitRepositoryEditView.getInformationTypesToShow.""" |
127 | 349 | 397 | ||
128 | === modified file 'lib/lp/code/configure.zcml' | |||
129 | --- lib/lp/code/configure.zcml 2015-06-12 17:29:59 +0000 | |||
130 | +++ lib/lp/code/configure.zcml 2015-06-15 10:02:42 +0000 | |||
131 | @@ -787,7 +787,8 @@ | |||
132 | 787 | permission="launchpad.View" | 787 | permission="launchpad.View" |
133 | 788 | interface="lp.app.interfaces.launchpad.IPrivacy | 788 | interface="lp.app.interfaces.launchpad.IPrivacy |
134 | 789 | lp.code.interfaces.gitrepository.IGitRepositoryView | 789 | lp.code.interfaces.gitrepository.IGitRepositoryView |
136 | 790 | lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" /> | 790 | lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes |
137 | 791 | lp.code.interfaces.gitrepository.IGitRepositoryEditableAttributes" /> | ||
138 | 791 | <require | 792 | <require |
139 | 792 | permission="launchpad.Moderate" | 793 | permission="launchpad.Moderate" |
140 | 793 | interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate" | 794 | interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate" |
141 | @@ -795,7 +796,8 @@ | |||
142 | 795 | set_attributes="date_last_modified" /> | 796 | set_attributes="date_last_modified" /> |
143 | 796 | <require | 797 | <require |
144 | 797 | permission="launchpad.Edit" | 798 | permission="launchpad.Edit" |
146 | 798 | interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" /> | 799 | interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" |
147 | 800 | set_schema="lp.code.interfaces.gitrepository.IGitRepositoryEditableAttributes" /> | ||
148 | 799 | </class> | 801 | </class> |
149 | 800 | <subscriber | 802 | <subscriber |
150 | 801 | for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent" | 803 | for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent" |
151 | 802 | 804 | ||
152 | === modified file 'lib/lp/code/errors.py' | |||
153 | --- lib/lp/code/errors.py 2015-05-19 11:29:24 +0000 | |||
154 | +++ lib/lp/code/errors.py 2015-06-15 10:02:42 +0000 | |||
155 | @@ -43,6 +43,7 @@ | |||
156 | 43 | 'InvalidNamespace', | 43 | 'InvalidNamespace', |
157 | 44 | 'NoLinkedBranch', | 44 | 'NoLinkedBranch', |
158 | 45 | 'NoSuchBranch', | 45 | 'NoSuchBranch', |
159 | 46 | 'NoSuchGitReference', | ||
160 | 46 | 'NoSuchGitRepository', | 47 | 'NoSuchGitRepository', |
161 | 47 | 'PrivateBranchRecipe', | 48 | 'PrivateBranchRecipe', |
162 | 48 | 'ReviewNotPending', | 49 | 'ReviewNotPending', |
163 | @@ -61,7 +62,10 @@ | |||
164 | 61 | from bzrlib.plugins.builder.recipe import RecipeParseError | 62 | from bzrlib.plugins.builder.recipe import RecipeParseError |
165 | 62 | from lazr.restful.declarations import error_status | 63 | from lazr.restful.declarations import error_status |
166 | 63 | 64 | ||
168 | 64 | from lp.app.errors import NameLookupFailed | 65 | from lp.app.errors import ( |
169 | 66 | NameLookupFailed, | ||
170 | 67 | NotFoundError, | ||
171 | 68 | ) | ||
172 | 65 | 69 | ||
173 | 66 | # Annotate the RecipeParseError's with a 400 webservice status. | 70 | # Annotate the RecipeParseError's with a 400 webservice status. |
174 | 67 | error_status(httplib.BAD_REQUEST)(RecipeParseError) | 71 | error_status(httplib.BAD_REQUEST)(RecipeParseError) |
175 | @@ -402,6 +406,21 @@ | |||
176 | 402 | _message_prefix = "No such Git repository" | 406 | _message_prefix = "No such Git repository" |
177 | 403 | 407 | ||
178 | 404 | 408 | ||
179 | 409 | class NoSuchGitReference(NotFoundError): | ||
180 | 410 | """Raised when we try to look up a Git reference that does not exist.""" | ||
181 | 411 | |||
182 | 412 | def __init__(self, repository, path): | ||
183 | 413 | self.repository = repository | ||
184 | 414 | self.path = path | ||
185 | 415 | self.message = ( | ||
186 | 416 | "The repository at %s does not contain a reference named '%s'." % | ||
187 | 417 | (repository.display_name, path)) | ||
188 | 418 | NotFoundError.__init__(self, self.message) | ||
189 | 419 | |||
190 | 420 | def __str__(self): | ||
191 | 421 | return self.message | ||
192 | 422 | |||
193 | 423 | |||
194 | 405 | @error_status(httplib.CONFLICT) | 424 | @error_status(httplib.CONFLICT) |
195 | 406 | class GitDefaultConflict(Exception): | 425 | class GitDefaultConflict(Exception): |
196 | 407 | """Raised when trying to set a Git repository as the default for | 426 | """Raised when trying to set a Git repository as the default for |
197 | 408 | 427 | ||
198 | === modified file 'lib/lp/code/interfaces/githosting.py' | |||
199 | --- lib/lp/code/interfaces/githosting.py 2015-06-15 09:49:42 +0000 | |||
200 | +++ lib/lp/code/interfaces/githosting.py 2015-06-15 10:02:42 +0000 | |||
201 | @@ -23,6 +23,20 @@ | |||
202 | 23 | other physical path. | 23 | other physical path. |
203 | 24 | """ | 24 | """ |
204 | 25 | 25 | ||
205 | 26 | def getProperties(path): | ||
206 | 27 | """Get properties of this repository. | ||
207 | 28 | |||
208 | 29 | :param path: Physical path of the repository on the hosting service. | ||
209 | 30 | :return: A dict of properties. | ||
210 | 31 | """ | ||
211 | 32 | |||
212 | 33 | def setProperties(path, **props): | ||
213 | 34 | """Set properties of this repository. | ||
214 | 35 | |||
215 | 36 | :param path: Physical path of the repository on the hosting service. | ||
216 | 37 | :param props: Properties to set. | ||
217 | 38 | """ | ||
218 | 39 | |||
219 | 26 | def getRefs(path): | 40 | def getRefs(path): |
220 | 27 | """Get all refs in this repository. | 41 | """Get all refs in this repository. |
221 | 28 | 42 | ||
222 | 29 | 43 | ||
223 | === modified file 'lib/lp/code/interfaces/gitrepository.py' | |||
224 | --- lib/lp/code/interfaces/gitrepository.py 2015-06-12 17:55:28 +0000 | |||
225 | +++ lib/lp/code/interfaces/gitrepository.py 2015-06-15 10:02:42 +0000 | |||
226 | @@ -552,6 +552,19 @@ | |||
227 | 552 | """ | 552 | """ |
228 | 553 | 553 | ||
229 | 554 | 554 | ||
230 | 555 | class IGitRepositoryEditableAttributes(Interface): | ||
231 | 556 | """IGitRepository attributes that can be edited. | ||
232 | 557 | |||
233 | 558 | These attributes need launchpad.View to see, and launchpad.Edit to change. | ||
234 | 559 | """ | ||
235 | 560 | |||
236 | 561 | default_branch = exported(TextLine( | ||
237 | 562 | title=_("Default branch"), required=False, readonly=False, | ||
238 | 563 | description=_( | ||
239 | 564 | "The full path to the default branch for this repository, e.g. " | ||
240 | 565 | "refs/heads/master."))) | ||
241 | 566 | |||
242 | 567 | |||
243 | 555 | class IGitRepositoryEdit(Interface): | 568 | class IGitRepositoryEdit(Interface): |
244 | 556 | """IGitRepository methods that require launchpad.Edit permission.""" | 569 | """IGitRepository methods that require launchpad.Edit permission.""" |
245 | 557 | 570 | ||
246 | @@ -619,7 +632,8 @@ | |||
247 | 619 | 632 | ||
248 | 620 | 633 | ||
249 | 621 | class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes, | 634 | class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes, |
251 | 622 | IGitRepositoryModerate, IGitRepositoryEdit): | 635 | IGitRepositoryModerate, IGitRepositoryEditableAttributes, |
252 | 636 | IGitRepositoryEdit): | ||
253 | 623 | """A Git repository.""" | 637 | """A Git repository.""" |
254 | 624 | 638 | ||
255 | 625 | # Mark repositories as exported entries for the Launchpad API. | 639 | # Mark repositories as exported entries for the Launchpad API. |
256 | 626 | 640 | ||
257 | === modified file 'lib/lp/code/model/githosting.py' | |||
258 | --- lib/lp/code/model/githosting.py 2015-06-15 09:49:42 +0000 | |||
259 | +++ lib/lp/code/model/githosting.py 2015-06-15 10:02:42 +0000 | |||
260 | @@ -68,6 +68,9 @@ | |||
261 | 68 | def _post(self, path, **kwargs): | 68 | def _post(self, path, **kwargs): |
262 | 69 | return self._request("post", path, **kwargs) | 69 | return self._request("post", path, **kwargs) |
263 | 70 | 70 | ||
264 | 71 | def _patch(self, path, **kwargs): | ||
265 | 72 | return self._request("patch", path, **kwargs) | ||
266 | 73 | |||
267 | 71 | def _delete(self, path, **kwargs): | 74 | def _delete(self, path, **kwargs): |
268 | 72 | return self._request("delete", path, **kwargs) | 75 | return self._request("delete", path, **kwargs) |
269 | 73 | 76 | ||
270 | @@ -83,6 +86,22 @@ | |||
271 | 83 | raise GitRepositoryCreationFault( | 86 | raise GitRepositoryCreationFault( |
272 | 84 | "Failed to create Git repository: %s" % unicode(e)) | 87 | "Failed to create Git repository: %s" % unicode(e)) |
273 | 85 | 88 | ||
274 | 89 | def getProperties(self, path): | ||
275 | 90 | """See `IGitHostingClient`.""" | ||
276 | 91 | try: | ||
277 | 92 | return self._get("/repo/%s" % path) | ||
278 | 93 | except Exception as e: | ||
279 | 94 | raise GitRepositoryScanFault( | ||
280 | 95 | "Failed to get properties of Git repository: %s" % unicode(e)) | ||
281 | 96 | |||
282 | 97 | def setProperties(self, path, **props): | ||
283 | 98 | """See `IGitHostingClient`.""" | ||
284 | 99 | try: | ||
285 | 100 | self._patch("/repo/%s" % path, json_data=props) | ||
286 | 101 | except Exception as e: | ||
287 | 102 | raise GitRepositoryScanFault( | ||
288 | 103 | "Failed to set properties of Git repository: %s" % unicode(e)) | ||
289 | 104 | |||
290 | 86 | def getRefs(self, path): | 105 | def getRefs(self, path): |
291 | 87 | """See `IGitHostingClient`.""" | 106 | """See `IGitHostingClient`.""" |
292 | 88 | try: | 107 | try: |
293 | 89 | 108 | ||
294 | === modified file 'lib/lp/code/model/gitjob.py' | |||
295 | --- lib/lp/code/model/gitjob.py 2015-06-12 17:55:28 +0000 | |||
296 | +++ lib/lp/code/model/gitjob.py 2015-06-15 10:02:42 +0000 | |||
297 | @@ -28,6 +28,7 @@ | |||
298 | 28 | classProvides, | 28 | classProvides, |
299 | 29 | implements, | 29 | implements, |
300 | 30 | ) | 30 | ) |
301 | 31 | from zope.security.proxy import removeSecurityProxy | ||
302 | 31 | 32 | ||
303 | 32 | from lp.app.errors import NotFoundError | 33 | from lp.app.errors import NotFoundError |
304 | 33 | from lp.code.interfaces.githosting import IGitHostingClient | 34 | from lp.code.interfaces.githosting import IGitHostingClient |
305 | @@ -212,6 +213,12 @@ | |||
306 | 212 | hosting_path, refs_to_upsert, logger=log) | 213 | hosting_path, refs_to_upsert, logger=log) |
307 | 213 | self.repository.synchroniseRefs( | 214 | self.repository.synchroniseRefs( |
308 | 214 | refs_to_upsert, refs_to_remove, logger=log) | 215 | refs_to_upsert, refs_to_remove, logger=log) |
309 | 216 | props = getUtility(IGitHostingClient).getProperties( | ||
310 | 217 | hosting_path) | ||
311 | 218 | # We don't want ref canonicalisation, nor do we want to send | ||
312 | 219 | # this change back to the hosting service. | ||
313 | 220 | removeSecurityProxy(self.repository)._default_branch = ( | ||
314 | 221 | props["default_branch"]) | ||
315 | 215 | except LostObjectError: | 222 | except LostObjectError: |
316 | 216 | log.info( | 223 | log.info( |
317 | 217 | "Skipping repository %s because it has been deleted." % | 224 | "Skipping repository %s because it has been deleted." % |
318 | 218 | 225 | ||
319 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
320 | --- lib/lp/code/model/gitrepository.py 2015-06-12 17:55:28 +0000 | |||
321 | +++ lib/lp/code/model/gitrepository.py 2015-06-15 10:02:42 +0000 | |||
322 | @@ -72,6 +72,7 @@ | |||
323 | 72 | CannotDeleteGitRepository, | 72 | CannotDeleteGitRepository, |
324 | 73 | GitDefaultConflict, | 73 | GitDefaultConflict, |
325 | 74 | GitTargetError, | 74 | GitTargetError, |
326 | 75 | NoSuchGitReference, | ||
327 | 75 | ) | 76 | ) |
328 | 76 | from lp.code.event.git import GitRefsUpdatedEvent | 77 | from lp.code.event.git import GitRefsUpdatedEvent |
329 | 77 | from lp.code.interfaces.branchmergeproposal import ( | 78 | from lp.code.interfaces.branchmergeproposal import ( |
330 | @@ -206,6 +207,8 @@ | |||
331 | 206 | owner_default = Bool(name='owner_default', allow_none=False) | 207 | owner_default = Bool(name='owner_default', allow_none=False) |
332 | 207 | target_default = Bool(name='target_default', allow_none=False) | 208 | target_default = Bool(name='target_default', allow_none=False) |
333 | 208 | 209 | ||
334 | 210 | _default_branch = Unicode(name='default_branch', allow_none=True) | ||
335 | 211 | |||
336 | 209 | def __init__(self, registrant, owner, target, name, information_type, | 212 | def __init__(self, registrant, owner, target, name, information_type, |
337 | 210 | date_created, reviewer=None, description=None): | 213 | date_created, reviewer=None, description=None): |
338 | 211 | super(GitRepository, self).__init__() | 214 | super(GitRepository, self).__init__() |
339 | @@ -407,6 +410,22 @@ | |||
340 | 407 | GitRef.repository_id == self.id, | 410 | GitRef.repository_id == self.id, |
341 | 408 | GitRef.path.startswith(u"refs/heads/")).order_by(GitRef.path) | 411 | GitRef.path.startswith(u"refs/heads/")).order_by(GitRef.path) |
342 | 409 | 412 | ||
343 | 413 | @property | ||
344 | 414 | def default_branch(self): | ||
345 | 415 | """See `IGitRepository`.""" | ||
346 | 416 | return self._default_branch | ||
347 | 417 | |||
348 | 418 | @default_branch.setter | ||
349 | 419 | def default_branch(self, value): | ||
350 | 420 | """See `IGitRepository`.""" | ||
351 | 421 | ref = self.getRefByPath(value) | ||
352 | 422 | if ref is None: | ||
353 | 423 | raise NoSuchGitReference(self, value) | ||
354 | 424 | if self._default_branch != ref.path: | ||
355 | 425 | self._default_branch = ref.path | ||
356 | 426 | getUtility(IGitHostingClient).setProperties( | ||
357 | 427 | self.getInternalPath(), default_branch=ref.path) | ||
358 | 428 | |||
359 | 410 | def getRefByPath(self, path): | 429 | def getRefByPath(self, path): |
360 | 411 | paths = [path] | 430 | paths = [path] |
361 | 412 | if not path.startswith(u"refs/heads/"): | 431 | if not path.startswith(u"refs/heads/"): |
362 | 413 | 432 | ||
363 | === modified file 'lib/lp/code/model/tests/test_gitjob.py' | |||
364 | --- lib/lp/code/model/tests/test_gitjob.py 2015-06-15 09:27:33 +0000 | |||
365 | +++ lib/lp/code/model/tests/test_gitjob.py 2015-06-15 10:02:42 +0000 | |||
366 | @@ -52,9 +52,10 @@ | |||
367 | 52 | 52 | ||
368 | 53 | implements(IGitHostingClient) | 53 | implements(IGitHostingClient) |
369 | 54 | 54 | ||
371 | 55 | def __init__(self, refs, commits): | 55 | def __init__(self, refs, commits, default_branch=u"refs/heads/master"): |
372 | 56 | self._refs = refs | 56 | self._refs = refs |
373 | 57 | self._commits = commits | 57 | self._commits = commits |
374 | 58 | self._default_branch = default_branch | ||
375 | 58 | 59 | ||
376 | 59 | def getRefs(self, paths): | 60 | def getRefs(self, paths): |
377 | 60 | return self._refs | 61 | return self._refs |
378 | @@ -62,6 +63,9 @@ | |||
379 | 62 | def getCommits(self, path, commit_oids, logger=None): | 63 | def getCommits(self, path, commit_oids, logger=None): |
380 | 63 | return self._commits | 64 | return self._commits |
381 | 64 | 65 | ||
382 | 66 | def getProperties(self, path): | ||
383 | 67 | return {u"default_branch": self._default_branch} | ||
384 | 68 | |||
385 | 65 | 69 | ||
386 | 66 | class TestGitJob(TestCaseWithFactory): | 70 | class TestGitJob(TestCaseWithFactory): |
387 | 67 | """Tests for `GitJob`.""" | 71 | """Tests for `GitJob`.""" |
388 | @@ -160,6 +164,7 @@ | |||
389 | 160 | with dbuser("branchscanner"): | 164 | with dbuser("branchscanner"): |
390 | 161 | JobRunner([job]).runAll() | 165 | JobRunner([job]).runAll() |
391 | 162 | self.assertRefsMatch(repository.refs, repository, paths) | 166 | self.assertRefsMatch(repository.refs, repository, paths) |
392 | 167 | self.assertEqual(u"refs/heads/master", repository.default_branch) | ||
393 | 163 | 168 | ||
394 | 164 | def test_logs_bad_ref_info(self): | 169 | def test_logs_bad_ref_info(self): |
395 | 165 | repository = self.factory.makeGitRepository() | 170 | repository = self.factory.makeGitRepository() |
396 | 166 | 171 | ||
397 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
398 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-06-12 17:55:28 +0000 | |||
399 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-06-15 10:02:42 +0000 | |||
400 | @@ -1217,6 +1217,36 @@ | |||
401 | 1217 | ) for path, sha1 in expected_sha1s] | 1217 | ) for path, sha1 in expected_sha1s] |
402 | 1218 | self.assertThat(repository.refs, MatchesSetwise(*matchers)) | 1218 | self.assertThat(repository.refs, MatchesSetwise(*matchers)) |
403 | 1219 | 1219 | ||
404 | 1220 | def test_set_default_branch(self): | ||
405 | 1221 | hosting_client = FakeMethod() | ||
406 | 1222 | hosting_client.setProperties = FakeMethod() | ||
407 | 1223 | self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient)) | ||
408 | 1224 | repository = self.factory.makeGitRepository() | ||
409 | 1225 | self.factory.makeGitRefs( | ||
410 | 1226 | repository=repository, | ||
411 | 1227 | paths=(u"refs/heads/master", u"refs/heads/new")) | ||
412 | 1228 | removeSecurityProxy(repository)._default_branch = u"refs/heads/master" | ||
413 | 1229 | with person_logged_in(repository.owner): | ||
414 | 1230 | repository.default_branch = u"new" | ||
415 | 1231 | self.assertEqual( | ||
416 | 1232 | [((repository.getInternalPath(),), | ||
417 | 1233 | {u"default_branch": u"refs/heads/new"})], | ||
418 | 1234 | hosting_client.setProperties.calls) | ||
419 | 1235 | self.assertEqual(u"refs/heads/new", repository.default_branch) | ||
420 | 1236 | |||
421 | 1237 | def test_set_default_branch_unchanged(self): | ||
422 | 1238 | hosting_client = FakeMethod() | ||
423 | 1239 | hosting_client.setProperties = FakeMethod() | ||
424 | 1240 | self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient)) | ||
425 | 1241 | repository = self.factory.makeGitRepository() | ||
426 | 1242 | self.factory.makeGitRefs( | ||
427 | 1243 | repository=repository, paths=[u"refs/heads/master"]) | ||
428 | 1244 | removeSecurityProxy(repository)._default_branch = u"refs/heads/master" | ||
429 | 1245 | with person_logged_in(repository.owner): | ||
430 | 1246 | repository.default_branch = u"master" | ||
431 | 1247 | self.assertEqual([], hosting_client.setProperties.calls) | ||
432 | 1248 | self.assertEqual(u"refs/heads/master", repository.default_branch) | ||
433 | 1249 | |||
434 | 1220 | 1250 | ||
435 | 1221 | class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory): | 1251 | class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory): |
436 | 1222 | """Test `IGitRepository.getAllowedInformationTypes`.""" | 1252 | """Test `IGitRepository.getAllowedInformationTypes`.""" |