Merge lp:~cjwatson/launchpad/git-ref-remote into lp:launchpad
- git-ref-remote
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18286 |
Proposed branch: | lp:~cjwatson/launchpad/git-ref-remote |
Merge into: | lp:launchpad |
Diff against target: |
614 lines (+343/-16) 11 files modified
lib/lp/app/browser/tales.py (+9/-0) lib/lp/code/browser/configure.zcml (+2/-0) lib/lp/code/browser/widgets/configure.zcml (+19/-0) lib/lp/code/browser/widgets/gitref.py (+87/-11) lib/lp/code/browser/widgets/tests/test_gitrefwidget.py (+56/-2) lib/lp/code/configure.zcml (+10/-0) lib/lp/code/enums.py (+7/-0) lib/lp/code/interfaces/gitref.py (+11/-0) lib/lp/code/model/gitref.py (+118/-2) lib/lp/security.py (+12/-0) lib/lp/testing/factory.py (+12/-1) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-ref-remote |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+312346@code.launchpad.net |
Commit message
Add GitRefRemote to encapsulate the notion of a ref in an external repository, and extend GitRefWidget to optionally support it.
Description of the change
This has no visible effect yet; the next branch in the series will hook it up.
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/app/browser/tales.py' |
2 | --- lib/lp/app/browser/tales.py 2016-07-12 13:32:10 +0000 |
3 | +++ lib/lp/app/browser/tales.py 2016-12-02 13:13:38 +0000 |
4 | @@ -1687,6 +1687,15 @@ |
5 | |
6 | _link_summary_template = '%(display_name)s' |
7 | |
8 | + def url(self, view_name=None, rootsite=None): |
9 | + """See `ObjectFormatterAPI`. |
10 | + |
11 | + `GitRefRemote` objects have no canonical URL. |
12 | + """ |
13 | + if self._context.repository_url is not None: |
14 | + return None |
15 | + return super(GitRefFormatterAPI, self).url(view_name, rootsite) |
16 | + |
17 | def _link_summary_values(self): |
18 | return {'display_name': self._context.display_name} |
19 | |
20 | |
21 | === modified file 'lib/lp/code/browser/configure.zcml' |
22 | --- lib/lp/code/browser/configure.zcml 2016-10-15 01:12:01 +0000 |
23 | +++ lib/lp/code/browser/configure.zcml 2016-12-02 13:13:38 +0000 |
24 | @@ -24,6 +24,8 @@ |
25 | PersonRevisionFeed ProductRevisionFeed ProjectRevisionFeed" |
26 | /> |
27 | |
28 | + <include package=".widgets"/> |
29 | + |
30 | <facet facet="branches"> |
31 | |
32 | <browser:defaultView |
33 | |
34 | === added file 'lib/lp/code/browser/widgets/configure.zcml' |
35 | --- lib/lp/code/browser/widgets/configure.zcml 1970-01-01 00:00:00 +0000 |
36 | +++ lib/lp/code/browser/widgets/configure.zcml 2016-12-02 13:13:38 +0000 |
37 | @@ -0,0 +1,19 @@ |
38 | +<!-- Copyright 2016 Canonical Ltd. This software is licensed under the |
39 | + GNU Affero General Public License version 3 (see the file LICENSE). |
40 | +--> |
41 | + |
42 | +<configure |
43 | + xmlns="http://namespaces.zope.org/zope" |
44 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
45 | + i18n_domain="launchpad"> |
46 | + |
47 | + <view |
48 | + type="zope.publisher.interfaces.browser.IBrowserRequest" |
49 | + for="lp.code.browser.widgets.gitref.IGitRepositoryField |
50 | + lp.services.webapp.vocabulary.IHugeVocabulary" |
51 | + provides="zope.formlib.interfaces.IInputWidget" |
52 | + factory="lp.code.browser.widgets.gitref.GitRepositoryPickerWidget" |
53 | + permission="zope.Public" |
54 | + /> |
55 | + |
56 | +</configure> |
57 | |
58 | === modified file 'lib/lp/code/browser/widgets/gitref.py' |
59 | --- lib/lp/code/browser/widgets/gitref.py 2015-09-25 17:25:23 +0000 |
60 | +++ lib/lp/code/browser/widgets/gitref.py 2016-12-02 13:13:38 +0000 |
61 | @@ -1,4 +1,4 @@ |
62 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
63 | +# Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
64 | # GNU Affero General Public License version 3 (see the file LICENSE). |
65 | |
66 | __metaclass__ = type |
67 | @@ -7,7 +7,9 @@ |
68 | 'GitRefWidget', |
69 | ] |
70 | |
71 | +import six |
72 | from z3c.ptcompat import ViewPageTemplateFile |
73 | +from zope.component import getUtility |
74 | from zope.formlib.interfaces import ( |
75 | ConversionError, |
76 | IInputWidget, |
77 | @@ -25,15 +27,79 @@ |
78 | Choice, |
79 | TextLine, |
80 | ) |
81 | +from zope.schema.interfaces import IChoice |
82 | |
83 | from lp.app.errors import UnexpectedFormData |
84 | from lp.app.validators import LaunchpadValidationError |
85 | +from lp.app.widgets.popup import VocabularyPickerWidget |
86 | +from lp.code.interfaces.gitref import IGitRefRemoteSet |
87 | +from lp.code.interfaces.gitrepository import IGitRepository |
88 | +from lp.services.fields import URIField |
89 | from lp.services.webapp.interfaces import ( |
90 | IAlwaysSubmittedWidget, |
91 | IMultiLineWidgetLayout, |
92 | ) |
93 | |
94 | |
95 | +class IGitRepositoryField(IChoice): |
96 | + pass |
97 | + |
98 | + |
99 | +@implementer(IGitRepositoryField) |
100 | +class GitRepositoryField(Choice): |
101 | + """A field identifying a Git repository. |
102 | + |
103 | + This may always be set to the unique name of a Launchpad-hosted |
104 | + repository. If `allow_external` is True, then it may also be set to a |
105 | + valid external repository URL. |
106 | + """ |
107 | + |
108 | + def __init__(self, allow_external=False, **kwargs): |
109 | + super(GitRepositoryField, self).__init__(**kwargs) |
110 | + if allow_external: |
111 | + self._uri_field = URIField( |
112 | + __name__=self.__name__, title=self.title, |
113 | + allowed_schemes=["git", "http", "https"], |
114 | + allow_userinfo=True, |
115 | + allow_port=True, |
116 | + allow_query=False, |
117 | + allow_fragment=False, |
118 | + trailing_slash=False) |
119 | + else: |
120 | + self._uri_field = None |
121 | + |
122 | + def set(self, object, value): |
123 | + if self._uri_field is not None and isinstance(value, six.string_types): |
124 | + try: |
125 | + self._uri_field.set(object, value) |
126 | + return |
127 | + except LaunchpadValidationError: |
128 | + pass |
129 | + super(GitRepositoryField, self).set(object, value) |
130 | + |
131 | + def _validate(self, value): |
132 | + if self._uri_field is not None and isinstance(value, six.string_types): |
133 | + try: |
134 | + self._uri_field._validate(value) |
135 | + return |
136 | + except LaunchpadValidationError: |
137 | + pass |
138 | + super(GitRepositoryField, self)._validate(value) |
139 | + |
140 | + |
141 | +class GitRepositoryPickerWidget(VocabularyPickerWidget): |
142 | + |
143 | + def convertTokensToValues(self, tokens): |
144 | + if self.context._uri_field is not None: |
145 | + try: |
146 | + self.context._uri_field._validate(tokens[0]) |
147 | + return [tokens[0]] |
148 | + except LaunchpadValidationError: |
149 | + pass |
150 | + return super(GitRepositoryPickerWidget, self).convertTokensToValues( |
151 | + tokens) |
152 | + |
153 | + |
154 | @implementer(IMultiLineWidgetLayout, IAlwaysSubmittedWidget, IInputWidget) |
155 | class GitRefWidget(BrowserWidget, InputWidget): |
156 | |
157 | @@ -41,13 +107,17 @@ |
158 | display_label = False |
159 | _widgets_set_up = False |
160 | |
161 | + # If True, allow entering external repository URLs. |
162 | + allow_external = False |
163 | + |
164 | def setUpSubWidgets(self): |
165 | if self._widgets_set_up: |
166 | return |
167 | fields = [ |
168 | - Choice( |
169 | + GitRepositoryField( |
170 | __name__="repository", title=u"Git repository", |
171 | - required=False, vocabulary="GitRepository"), |
172 | + required=False, vocabulary="GitRepository", |
173 | + allow_external=self.allow_external), |
174 | TextLine(__name__="path", title=u"Git branch", required=False), |
175 | ] |
176 | for field in fields: |
177 | @@ -59,7 +129,10 @@ |
178 | """See `IWidget`.""" |
179 | self.setUpSubWidgets() |
180 | if value is not None: |
181 | - self.repository_widget.setRenderedValue(value.repository) |
182 | + if self.allow_external and value.repository_url is not None: |
183 | + self.repository_widget.setRenderedValue(value.repository_url) |
184 | + else: |
185 | + self.repository_widget.setRenderedValue(value.repository) |
186 | self.path_widget.setRenderedValue(value.path) |
187 | else: |
188 | self.repository_widget.setRenderedValue(None) |
189 | @@ -112,13 +185,16 @@ |
190 | "Please enter a Git branch path.")) |
191 | else: |
192 | return |
193 | - ref = repository.getRefByPath(path) |
194 | - if ref is None: |
195 | - raise WidgetInputError( |
196 | - self.name, self.label, |
197 | - LaunchpadValidationError( |
198 | - "The repository at %s does not contain a branch named " |
199 | - "'%s'." % (repository.display_name, path))) |
200 | + if self.allow_external and not IGitRepository.providedBy(repository): |
201 | + ref = getUtility(IGitRefRemoteSet).new(repository, path) |
202 | + else: |
203 | + ref = repository.getRefByPath(path) |
204 | + if ref is None: |
205 | + raise WidgetInputError( |
206 | + self.name, self.label, |
207 | + LaunchpadValidationError( |
208 | + "The repository at %s does not contain a branch named " |
209 | + "'%s'." % (repository.display_name, path))) |
210 | return ref |
211 | |
212 | def error(self): |
213 | |
214 | === modified file 'lib/lp/code/browser/widgets/tests/test_gitrefwidget.py' |
215 | --- lib/lp/code/browser/widgets/tests/test_gitrefwidget.py 2015-09-25 17:25:23 +0000 |
216 | +++ lib/lp/code/browser/widgets/tests/test_gitrefwidget.py 2016-12-02 13:13:38 +0000 |
217 | @@ -1,10 +1,14 @@ |
218 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
219 | +# Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
220 | # GNU Affero General Public License version 3 (see the file LICENSE). |
221 | |
222 | __metaclass__ = type |
223 | |
224 | from BeautifulSoup import BeautifulSoup |
225 | from lazr.restful.fields import Reference |
226 | +from testscenarios import ( |
227 | + load_tests_apply_scenarios, |
228 | + WithScenarios, |
229 | + ) |
230 | from zope.formlib.interfaces import ( |
231 | IBrowserWidget, |
232 | IInputWidget, |
233 | @@ -36,10 +40,15 @@ |
234 | pass |
235 | |
236 | |
237 | -class TestGitRefWidget(TestCaseWithFactory): |
238 | +class TestGitRefWidget(WithScenarios, TestCaseWithFactory): |
239 | |
240 | layer = DatabaseFunctionalLayer |
241 | |
242 | + scenarios = [ |
243 | + ("disallow_external", {"allow_external": False}), |
244 | + ("allow_external", {"allow_external": True}), |
245 | + ] |
246 | + |
247 | def setUp(self): |
248 | super(TestGitRefWidget, self).setUp() |
249 | field = Reference( |
250 | @@ -48,6 +57,7 @@ |
251 | field = field.bind(self.context) |
252 | request = LaunchpadTestRequest() |
253 | self.widget = GitRefWidget(field, request) |
254 | + self.widget.allow_external = self.allow_external |
255 | |
256 | def test_implements(self): |
257 | self.assertTrue(verifyObject(IBrowserWidget, self.widget)) |
258 | @@ -85,6 +95,19 @@ |
259 | ref.repository, self.widget.repository_widget._getCurrentValue()) |
260 | self.assertEqual(ref.path, self.widget.path_widget._getCurrentValue()) |
261 | |
262 | + def test_setRenderedValue_external(self): |
263 | + # If allow_external is True, providing a reference in an external |
264 | + # repository works. |
265 | + self.widget.setUpSubWidgets() |
266 | + ref = self.factory.makeGitRefRemote() |
267 | + self.widget.setRenderedValue(ref) |
268 | + repository_value = self.widget.repository_widget._getCurrentValue() |
269 | + if self.allow_external: |
270 | + self.assertEqual(ref.repository_url, repository_value) |
271 | + else: |
272 | + self.assertIsNone(repository_value) |
273 | + self.assertEqual(ref.path, self.widget.path_widget._getCurrentValue()) |
274 | + |
275 | def test_hasInput_false(self): |
276 | # hasInput is false when the widget's name is not in the form data. |
277 | self.widget.request = LaunchpadTestRequest(form={}) |
278 | @@ -144,6 +167,18 @@ |
279 | "There is no Git repository named 'non-existent' registered in " |
280 | "Launchpad.") |
281 | |
282 | + def test_getInputValue_repository_invalid_url(self): |
283 | + # An error is raised when the repository field is set to an invalid |
284 | + # URL. |
285 | + form = { |
286 | + "field.git_ref.repository": "file:///etc/shadow", |
287 | + "field.git_ref.path": "master", |
288 | + } |
289 | + self.assertGetInputValueError( |
290 | + form, |
291 | + "There is no Git repository named 'file:///etc/shadow' " |
292 | + "registered in Launchpad.") |
293 | + |
294 | def test_getInputValue_path_empty(self): |
295 | # An error is raised when the path field is empty. |
296 | repository = self.factory.makeGitRepository() |
297 | @@ -197,6 +232,22 @@ |
298 | self.widget.request = LaunchpadTestRequest(form=form) |
299 | self.assertEqual(ref, self.widget.getInputValue()) |
300 | |
301 | + def test_getInputValue_valid_url(self): |
302 | + # If allow_external is True, the repository may be a URL. |
303 | + ref = self.factory.makeGitRefRemote() |
304 | + form = { |
305 | + "field.git_ref.repository": ref.repository_url, |
306 | + "field.git_ref.path": ref.path, |
307 | + } |
308 | + if self.allow_external: |
309 | + self.widget.request = LaunchpadTestRequest(form=form) |
310 | + self.assertEqual(ref, self.widget.getInputValue()) |
311 | + else: |
312 | + self.assertGetInputValueError( |
313 | + form, |
314 | + "There is no Git repository named '%s' registered in " |
315 | + "Launchpad." % ref.repository_url) |
316 | + |
317 | def test_call(self): |
318 | # The __call__ method sets up the widgets. |
319 | markup = self.widget() |
320 | @@ -207,3 +258,6 @@ |
321 | ids = [field["id"] for field in fields] |
322 | self.assertContentEqual( |
323 | ["field.git_ref.repository", "field.git_ref.path"], ids) |
324 | + |
325 | + |
326 | +load_tests = load_tests_apply_scenarios |
327 | |
328 | === modified file 'lib/lp/code/configure.zcml' |
329 | --- lib/lp/code/configure.zcml 2016-10-13 12:43:14 +0000 |
330 | +++ lib/lp/code/configure.zcml 2016-12-02 13:13:38 +0000 |
331 | @@ -901,6 +901,16 @@ |
332 | permission="launchpad.View" |
333 | interface="lp.code.interfaces.gitref.IGitRef" /> |
334 | </class> |
335 | + <class class="lp.code.model.gitref.GitRefRemote"> |
336 | + <require |
337 | + permission="launchpad.View" |
338 | + interface="lp.code.interfaces.gitref.IGitRef" /> |
339 | + </class> |
340 | + <securedutility |
341 | + component="lp.code.model.gitref.GitRefRemote" |
342 | + provides="lp.code.interfaces.gitref.IGitRefRemoteSet"> |
343 | + <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" /> |
344 | + </securedutility> |
345 | |
346 | <!-- GitCollection --> |
347 | |
348 | |
349 | === modified file 'lib/lp/code/enums.py' |
350 | --- lib/lp/code/enums.py 2016-10-05 15:12:48 +0000 |
351 | +++ lib/lp/code/enums.py 2016-12-02 13:13:38 +0000 |
352 | @@ -135,6 +135,13 @@ |
353 | repository and is made available through Launchpad. |
354 | """) |
355 | |
356 | + REMOTE = DBItem(4, """ |
357 | + Remote |
358 | + |
359 | + Registered in Launchpad with an external location, |
360 | + but is not to be mirrored, nor available through Launchpad. |
361 | + """) |
362 | + |
363 | |
364 | class GitObjectType(DBEnumeratedType): |
365 | """Git Object Type |
366 | |
367 | === modified file 'lib/lp/code/interfaces/gitref.py' |
368 | --- lib/lp/code/interfaces/gitref.py 2016-12-02 13:01:53 +0000 |
369 | +++ lib/lp/code/interfaces/gitref.py 2016-12-02 13:13:38 +0000 |
370 | @@ -8,6 +8,7 @@ |
371 | __all__ = [ |
372 | 'IGitRef', |
373 | 'IGitRefBatchNavigator', |
374 | + 'IGitRefRemoteSet', |
375 | ] |
376 | |
377 | from lazr.restful.declarations import ( |
378 | @@ -67,6 +68,9 @@ |
379 | schema=Interface, |
380 | description=_("The Git repository containing this reference."))) |
381 | |
382 | + repository_url = Attribute( |
383 | + "The repository URL, if this is a reference in a remote repository.") |
384 | + |
385 | path = exported(TextLine( |
386 | title=_("Path"), required=True, readonly=True, |
387 | description=_( |
388 | @@ -381,3 +385,10 @@ |
389 | |
390 | class IGitRefBatchNavigator(ITableBatchNavigator): |
391 | pass |
392 | + |
393 | + |
394 | +class IGitRefRemoteSet(Interface): |
395 | + """Interface allowing creation of `GitRefRemote`s.""" |
396 | + |
397 | + def new(repository_url, path): |
398 | + """Create a new remote reference.""" |
399 | |
400 | === modified file 'lib/lp/code/model/gitref.py' |
401 | --- lib/lp/code/model/gitref.py 2016-12-02 13:01:53 +0000 |
402 | +++ lib/lp/code/model/gitref.py 2016-12-02 13:13:38 +0000 |
403 | @@ -5,6 +5,7 @@ |
404 | __all__ = [ |
405 | 'GitRef', |
406 | 'GitRefFrozen', |
407 | + 'GitRefRemote', |
408 | ] |
409 | |
410 | from datetime import datetime |
411 | @@ -25,13 +26,18 @@ |
412 | ) |
413 | from zope.component import getUtility |
414 | from zope.event import notify |
415 | -from zope.interface import implementer |
416 | +from zope.interface import ( |
417 | + implementer, |
418 | + provider, |
419 | + ) |
420 | from zope.security.proxy import removeSecurityProxy |
421 | |
422 | +from lp.app.enums import InformationType |
423 | from lp.app.errors import NotFoundError |
424 | from lp.code.enums import ( |
425 | BranchMergeProposalStatus, |
426 | GitObjectType, |
427 | + GitRepositoryType, |
428 | ) |
429 | from lp.code.errors import ( |
430 | BranchMergeProposalExists, |
431 | @@ -46,7 +52,10 @@ |
432 | ) |
433 | from lp.code.interfaces.gitcollection import IAllGitRepositories |
434 | from lp.code.interfaces.githosting import IGitHostingClient |
435 | -from lp.code.interfaces.gitref import IGitRef |
436 | +from lp.code.interfaces.gitref import ( |
437 | + IGitRef, |
438 | + IGitRefRemoteSet, |
439 | + ) |
440 | from lp.code.model.branchmergeproposal import ( |
441 | BranchMergeProposal, |
442 | BranchMergeProposalGetter, |
443 | @@ -69,6 +78,8 @@ |
444 | require a database record. |
445 | """ |
446 | |
447 | + repository_url = None |
448 | + |
449 | @property |
450 | def display_name(self): |
451 | """See `IGitRef`.""" |
452 | @@ -573,3 +584,108 @@ |
453 | |
454 | def __hash__(self): |
455 | return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1) |
456 | + |
457 | + |
458 | +@implementer(IGitRef) |
459 | +@provider(IGitRefRemoteSet) |
460 | +class GitRefRemote(GitRefMixin): |
461 | + """A reference in a remotely-hosted Git repository. |
462 | + |
463 | + We can do very little with these - for example, we don't know their tip |
464 | + commit ID - but it's useful to be able to pass the repository URL and |
465 | + path around as a unit. |
466 | + """ |
467 | + |
468 | + def __init__(self, repository_url, path): |
469 | + self.repository_url = repository_url |
470 | + self.path = path |
471 | + |
472 | + @classmethod |
473 | + def new(cls, repository_url, path): |
474 | + """See `IGitRefRemoteSet`.""" |
475 | + return cls(repository_url, path) |
476 | + |
477 | + def _unimplemented(self, *args, **kwargs): |
478 | + raise NotImplementedError("Not implemented for remote repositories.") |
479 | + |
480 | + repository = None |
481 | + commit_sha1 = property(_unimplemented) |
482 | + object_type = property(_unimplemented) |
483 | + author = None |
484 | + author_date = None |
485 | + committer = None |
486 | + committer_date = None |
487 | + commit_message = None |
488 | + commit_message_first_line = property(_unimplemented) |
489 | + |
490 | + @property |
491 | + def identity(self): |
492 | + """See `IGitRef`.""" |
493 | + return "%s %s" % (self.repository_url, self.name) |
494 | + |
495 | + @property |
496 | + def unique_name(self): |
497 | + """See `IGitRef`.""" |
498 | + return "%s %s" % (self.repository_url, self.name) |
499 | + |
500 | + repository_type = GitRepositoryType.REMOTE |
501 | + owner = property(_unimplemented) |
502 | + target = property(_unimplemented) |
503 | + namespace = property(_unimplemented) |
504 | + getCodebrowseUrl = _unimplemented |
505 | + getCodebrowseUrlForRevision = _unimplemented |
506 | + information_type = InformationType.PUBLIC |
507 | + private = False |
508 | + |
509 | + def visibleByUser(self, user): |
510 | + """See `IGitRef`.""" |
511 | + return True |
512 | + |
513 | + transitionToInformationType = _unimplemented |
514 | + reviewer = property(_unimplemented) |
515 | + code_reviewer = property(_unimplemented) |
516 | + isPersonTrustedReviewer = _unimplemented |
517 | + |
518 | + @property |
519 | + def subscriptions(self): |
520 | + """See `IGitRef`.""" |
521 | + return [] |
522 | + |
523 | + @property |
524 | + def subscribers(self): |
525 | + """See `IGitRef`.""" |
526 | + return [] |
527 | + |
528 | + subscribe = _unimplemented |
529 | + getSubscription = _unimplemented |
530 | + unsubscribe = _unimplemented |
531 | + getNotificationRecipients = _unimplemented |
532 | + landing_targets = property(_unimplemented) |
533 | + landing_candidates = property(_unimplemented) |
534 | + dependent_landings = property(_unimplemented) |
535 | + addLandingTarget = _unimplemented |
536 | + createMergeProposal = _unimplemented |
537 | + getMergeProposals = _unimplemented |
538 | + getDependentMergeProposals = _unimplemented |
539 | + pending_writes = False |
540 | + |
541 | + def getCommits(self, *args, **kwargs): |
542 | + """See `IGitRef`.""" |
543 | + return [] |
544 | + |
545 | + def getLatestCommits(self, *args, **kwargs): |
546 | + """See `IGitRef`.""" |
547 | + return [] |
548 | + |
549 | + @property |
550 | + def recipes(self): |
551 | + """See `IHasRecipes`.""" |
552 | + return [] |
553 | + |
554 | + def __eq__(self, other): |
555 | + return ( |
556 | + self.repository_url == other.repository_url and |
557 | + self.path == other.path) |
558 | + |
559 | + def __ne__(self, other): |
560 | + return not self == other |
561 | |
562 | === modified file 'lib/lp/security.py' |
563 | --- lib/lp/security.py 2016-11-15 11:44:27 +0000 |
564 | +++ lib/lp/security.py 2016-12-02 13:13:38 +0000 |
565 | @@ -2298,6 +2298,18 @@ |
566 | def __init__(self, obj): |
567 | super(ViewGitRef, self).__init__(obj, obj.repository) |
568 | |
569 | + def checkAuthenticated(self, user): |
570 | + if self.obj.repository is not None: |
571 | + return super(ViewGitRef, self).checkAuthenticated(user) |
572 | + else: |
573 | + return True |
574 | + |
575 | + def checkUnauthenticated(self): |
576 | + if self.obj.repository is not None: |
577 | + return super(ViewGitRef, self).checkUnauthenticated() |
578 | + else: |
579 | + return True |
580 | + |
581 | |
582 | class EditGitRef(DelegatedAuthorization): |
583 | """Anyone who can edit a Git repository can edit references within it.""" |
584 | |
585 | === modified file 'lib/lp/testing/factory.py' |
586 | --- lib/lp/testing/factory.py 2016-11-07 18:14:32 +0000 |
587 | +++ lib/lp/testing/factory.py 2016-12-02 13:13:38 +0000 |
588 | @@ -124,7 +124,10 @@ |
589 | from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet |
590 | from lp.code.interfaces.codeimportresult import ICodeImportResultSet |
591 | from lp.code.interfaces.gitnamespace import get_git_namespace |
592 | -from lp.code.interfaces.gitref import IGitRef |
593 | +from lp.code.interfaces.gitref import ( |
594 | + IGitRef, |
595 | + IGitRefRemoteSet, |
596 | + ) |
597 | from lp.code.interfaces.gitrepository import IGitRepository |
598 | from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch |
599 | from lp.code.interfaces.revision import IRevisionSet |
600 | @@ -1809,6 +1812,14 @@ |
601 | refs_info, get_objects=True)} |
602 | return [refs_by_path[path] for path in paths] |
603 | |
604 | + def makeGitRefRemote(self, repository_url=None, path=None): |
605 | + """Create an object representing a ref in a remote repository.""" |
606 | + if repository_url is None: |
607 | + repository_url = self.getUniqueURL().decode('utf-8') |
608 | + if path is None: |
609 | + path = self.getUniqueString('refs/heads/path').decode('utf-8') |
610 | + return getUtility(IGitRefRemoteSet).new(repository_url, path) |
611 | + |
612 | def makeBug(self, target=None, owner=None, bug_watch_url=None, |
613 | information_type=None, date_closed=None, title=None, |
614 | date_created=None, description=None, comment=None, |