Merge lp:~cjwatson/launchpad/codeimport-git-model into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18223
Proposed branch: lp:~cjwatson/launchpad/codeimport-git-model
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-repository-type
Diff against target: 1495 lines (+427/-159)
16 files modified
lib/lp/code/configure.zcml (+2/-0)
lib/lp/code/doc/codeimport.txt (+14/-4)
lib/lp/code/emailtemplates/code-import-status-updated.txt (+1/-1)
lib/lp/code/emailtemplates/new-code-import.txt (+1/-1)
lib/lp/code/enums.py (+21/-1)
lib/lp/code/interfaces/codeimport.py (+16/-4)
lib/lp/code/interfaces/gitnamespace.py (+8/-2)
lib/lp/code/mail/codeimport.py (+11/-11)
lib/lp/code/mail/tests/test_codeimport.py (+35/-8)
lib/lp/code/model/codeimport.py (+69/-18)
lib/lp/code/model/codeimportjob.py (+14/-13)
lib/lp/code/model/gitnamespace.py (+4/-1)
lib/lp/code/model/hasbranches.py (+6/-2)
lib/lp/code/model/tests/test_codeimport.py (+200/-85)
lib/lp/registry/browser/product.py (+5/-1)
lib/lp/testing/factory.py (+20/-7)
To merge this branch: bzr merge lp:~cjwatson/launchpad/codeimport-git-model
Reviewer Review Type Date Requested Status
William Grant (community) code Approve
Review via email: mp+307466@code.launchpad.net

Commit message

Add basic model for code imports that target Git.

Description of the change

Add basic model for code imports that target Git.

This is overlong, but since I had to introduce the concept of a different target revision control system type here, it was tough to make it any shorter. ICodeImport.git_repository is exported read-only on the webservice, but that should be pretty harmless; nothing else here should be user-visible yet.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

A relatively clean place way to split this would have been s/branch/target/ in an earlier branch. But it's easy enough to scroll over.

review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/configure.zcml'
2--- lib/lp/code/configure.zcml 2016-06-20 20:32:36 +0000
3+++ lib/lp/code/configure.zcml 2016-10-03 17:03:17 +0000
4@@ -633,6 +633,8 @@
5 <allow attributes="id
6 date_created
7 branch
8+ git_repository
9+ target
10 registrant
11 owner
12 assignee
13
14=== modified file 'lib/lp/code/doc/codeimport.txt'
15--- lib/lp/code/doc/codeimport.txt 2016-10-03 17:03:12 +0000
16+++ lib/lp/code/doc/codeimport.txt 2016-10-03 17:03:17 +0000
17@@ -52,8 +52,12 @@
18
19 The rcs_type field, which indicates whether the import is from CVS or
20 Subversion, takes values from the 'RevisionControlSystems' vocabulary.
21+Similarly, target_rcs_type takes values from 'TargetRevisionControlSystems'.
22
23- >>> from lp.code.enums import RevisionControlSystems
24+ >>> from lp.code.enums import (
25+ ... RevisionControlSystems,
26+ ... TargetRevisionControlSystems,
27+ ... )
28 >>> for item in RevisionControlSystems:
29 ... print item.title
30 Concurrent Versions System
31@@ -62,6 +66,10 @@
32 Git
33 Mercurial
34 Bazaar
35+ >>> for item in TargetRevisionControlSystems:
36+ ... print item.title
37+ Bazaar
38+ Git
39
40
41 Import from CVS
42@@ -71,12 +79,14 @@
43 in the repository, known as the "module".
44
45 >>> cvs = RevisionControlSystems.CVS
46+ >>> target_bzr = TargetRevisionControlSystems.BZR
47 >>> cvs_root = ':pserver:anonymous@cvs.example.com:/cvsroot'
48 >>> cvs_module = 'hello'
49 >>> context = factory.makeProduct(name='widget')
50 >>> cvs_import = code_import_set.new(
51 ... registrant=nopriv, context=context, branch_name='trunk-cvs',
52- ... rcs_type=cvs, cvs_root=cvs_root, cvs_module=cvs_module)
53+ ... rcs_type=cvs, cvs_root=cvs_root, cvs_module=cvs_module,
54+ ... target_rcs_type=target_bzr)
55 >>> verifyObject(ICodeImport, removeSecurityProxy(cvs_import))
56 True
57
58@@ -136,7 +146,7 @@
59 >>> svn_url = 'svn://svn.example.com/trunk'
60 >>> svn_import = code_import_set.new(
61 ... registrant=nopriv, context=context, branch_name='trunk-svn',
62- ... rcs_type=svn, url=svn_url)
63+ ... rcs_type=svn, url=svn_url, target_rcs_type=target_bzr)
64 >>> verifyObject(ICodeImport, removeSecurityProxy(svn_import))
65 True
66
67@@ -164,7 +174,7 @@
68 >>> git_url = 'git://git.example.com/hello.git'
69 >>> git_import = code_import_set.new(
70 ... registrant=nopriv, context=context, branch_name='trunk-git',
71- ... rcs_type=git, url=git_url)
72+ ... rcs_type=git, url=git_url, target_rcs_type=target_bzr)
73 >>> verifyObject(ICodeImport, removeSecurityProxy(git_import))
74 True
75
76
77=== modified file 'lib/lp/code/emailtemplates/code-import-status-updated.txt'
78--- lib/lp/code/emailtemplates/code-import-status-updated.txt 2011-12-18 22:31:46 +0000
79+++ lib/lp/code/emailtemplates/code-import-status-updated.txt 2016-10-03 17:03:17 +0000
80@@ -3,5 +3,5 @@
81 %(body)s
82
83 --
84-%(branch)s
85+%(target)s
86 %(rationale)s%(unsubscribe)s
87
88=== modified file 'lib/lp/code/emailtemplates/new-code-import.txt'
89--- lib/lp/code/emailtemplates/new-code-import.txt 2011-12-18 22:31:46 +0000
90+++ lib/lp/code/emailtemplates/new-code-import.txt 2016-10-03 17:03:17 +0000
91@@ -1,5 +1,5 @@
92 A new %(rcs_type)s code import has been requested by %(person)s:
93- %(branch)s
94+ %(target)s
95 from
96 %(location)s
97
98
99=== modified file 'lib/lp/code/enums.py'
100--- lib/lp/code/enums.py 2016-10-03 17:03:12 +0000
101+++ lib/lp/code/enums.py 2016-10-03 17:03:17 +0000
102@@ -1,4 +1,4 @@
103-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
104+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
105 # GNU Affero General Public License version 3 (see the file LICENSE).
106
107 """Enumerations used in the lp/code modules."""
108@@ -24,6 +24,7 @@
109 'GitRepositoryType',
110 'NON_CVS_RCS_TYPES',
111 'RevisionControlSystems',
112+ 'TargetRevisionControlSystems',
113 ]
114
115 from lazr.enum import (
116@@ -400,6 +401,25 @@
117 """)
118
119
120+class TargetRevisionControlSystems(EnumeratedType):
121+ """Target Revision Control Systems
122+
123+ Revision control systems that can be the target of a code import.
124+ """
125+
126+ BZR = Item("""
127+ Bazaar
128+
129+ Import to Bazaar.
130+ """)
131+
132+ GIT = Item("""
133+ Git
134+
135+ Import to Git.
136+ """)
137+
138+
139 class CodeImportReviewStatus(DBEnumeratedType):
140 """CodeImport review status.
141
142
143=== modified file 'lib/lp/code/interfaces/codeimport.py'
144--- lib/lp/code/interfaces/codeimport.py 2016-09-30 13:27:27 +0000
145+++ lib/lp/code/interfaces/codeimport.py 2016-10-03 17:03:17 +0000
146@@ -43,6 +43,7 @@
147 RevisionControlSystems,
148 )
149 from lp.code.interfaces.branch import IBranch
150+from lp.code.interfaces.gitrepository import IGitRepository
151 from lp.services.fields import (
152 PublicPersonChoice,
153 URIField,
154@@ -86,10 +87,18 @@
155
156 branch = exported(
157 ReferenceChoice(
158- title=_('Branch'), required=True, readonly=True,
159+ title=_('Branch'), required=False, readonly=True,
160 vocabulary='Branch', schema=IBranch,
161 description=_("The Bazaar branch produced by the "
162 "import system.")))
163+ git_repository = exported(
164+ ReferenceChoice(
165+ title=_('Git repository'), required=False, readonly=True,
166+ vocabulary='GitRepository', schema=IGitRepository,
167+ description=_(
168+ "The Git repository produced by the import system.")))
169+ target = Attribute(
170+ "The branch/repository produced by the import system (VCS-agnostic).")
171
172 registrant = PublicPersonChoice(
173 title=_('Registrant'), required=True, readonly=True,
174@@ -220,8 +229,8 @@
175 class ICodeImportSet(Interface):
176 """Interface representing the set of code imports."""
177
178- def new(registrant, context, branch_name, rcs_type, url=None,
179- cvs_root=None, cvs_module=None, review_status=None,
180+ def new(registrant, context, branch_name, rcs_type, target_rcs_type,
181+ url=None, cvs_root=None, cvs_module=None, review_status=None,
182 owner=None):
183 """Create a new CodeImport.
184
185@@ -238,7 +247,10 @@
186 """
187
188 def getByBranch(branch):
189- """Get the CodeImport, if any, associated to a Branch."""
190+ """Get the CodeImport, if any, associated with a Branch."""
191+
192+ def getByGitRepository(repository):
193+ """Get the CodeImport, if any, associated with a GitRepository."""
194
195 def getByCVSDetails(cvs_root, cvs_module):
196 """Get the CodeImport with the specified CVS details."""
197
198=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
199--- lib/lp/code/interfaces/gitnamespace.py 2016-10-03 17:03:12 +0000
200+++ lib/lp/code/interfaces/gitnamespace.py 2016-10-03 17:03:17 +0000
201@@ -1,4 +1,4 @@
202-# Copyright 2015 Canonical Ltd. This software is licensed under the
203+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
204 # GNU Affero General Public License version 3 (see the file LICENSE).
205
206 """Interface for a Git repository namespace."""
207@@ -22,6 +22,7 @@
208 from lp.registry.interfaces.distributionsourcepackage import (
209 IDistributionSourcePackage,
210 )
211+from lp.registry.interfaces.person import IPerson
212 from lp.registry.interfaces.product import IProduct
213
214
215@@ -96,6 +97,9 @@
216 supports_merge_proposals = Attribute(
217 "Does this namespace support merge proposals at all?")
218
219+ supports_code_imports = Attribute(
220+ "Does this namespace support code imports at all?")
221+
222 allow_recipe_name_from_target = Attribute(
223 "Can recipe names reasonably be generated from the target name "
224 "rather than the branch name?")
225@@ -188,8 +192,10 @@
226 return getUtility(IGitNamespaceSet).get(
227 owner, distribution=target.distribution,
228 sourcepackagename=target.sourcepackagename)
229+ elif IPerson.providedBy(target):
230+ return getUtility(IGitNamespaceSet).get(owner)
231 else:
232- return getUtility(IGitNamespaceSet).get(owner)
233+ raise AssertionError("No Git namespace defined for %s" % target)
234
235
236 # Marker for references to Git URL layouts: ##GITNAMESPACE##
237
238=== modified file 'lib/lp/code/mail/codeimport.py'
239--- lib/lp/code/mail/codeimport.py 2016-10-03 17:03:12 +0000
240+++ lib/lp/code/mail/codeimport.py 2016-10-03 17:03:17 +0000
241@@ -39,7 +39,7 @@
242 # test.
243 return
244 user = IPerson(event.user)
245- subject = 'New code import: %s' % code_import.branch.unique_name
246+ subject = 'New code import: %s' % code_import.target.unique_name
247 if code_import.rcs_type == RevisionControlSystems.CVS:
248 location = '%s, %s' % (code_import.cvs_root, code_import.cvs_module)
249 else:
250@@ -52,7 +52,7 @@
251 }
252 body = get_email_template('new-code-import.txt', app='code') % {
253 'person': code_import.registrant.displayname,
254- 'branch': canonical_url(code_import.branch),
255+ 'target': canonical_url(code_import.target),
256 'rcs_type': rcs_type_map[code_import.rcs_type],
257 'location': location,
258 }
259@@ -61,7 +61,7 @@
260 user.displayname, user.preferredemail.email)
261
262 vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
263- headers = {'X-Launchpad-Branch': code_import.branch.unique_name,
264+ headers = {'X-Launchpad-Branch': code_import.target.unique_name,
265 'X-Launchpad-Message-Rationale':
266 'Operator @%s' % vcs_imports.name,
267 'X-Launchpad-Message-For': vcs_imports.name,
268@@ -103,7 +103,7 @@
269 raise AssertionError('Unexpected review status for code import.')
270
271 details_change_prefix = '\n'.join(textwrap.wrap(
272- "%s is now being imported from:" % code_import.branch.unique_name))
273+ "%s is now being imported from:" % code_import.target.unique_name))
274 if code_import.rcs_type == RevisionControlSystems.CVS:
275 if (CodeImportEventDataType.OLD_CVS_ROOT in event_data or
276 CodeImportEventDataType.OLD_CVS_MODULE in event_data):
277@@ -142,26 +142,26 @@
278
279
280 def code_import_updated(code_import, event, new_whiteboard, person):
281- """Email the branch subscribers, and the vcs-imports team with new status.
282+ """Email the target subscribers, and the vcs-imports team with new status.
283 """
284- branch = code_import.branch
285- recipients = branch.getNotificationRecipients()
286+ target = code_import.target
287+ recipients = target.getNotificationRecipients()
288 # Add in the vcs-imports user.
289 vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
290 herder_rationale = 'Operator @%s' % vcs_imports.name
291 recipients.add(vcs_imports, None, herder_rationale)
292
293- headers = {'X-Launchpad-Branch': branch.unique_name}
294+ headers = {'X-Launchpad-Branch': target.unique_name}
295
296 subject = 'Code import %s status: %s' % (
297- code_import.branch.unique_name, code_import.review_status.title)
298+ code_import.target.unique_name, code_import.review_status.title)
299
300 email_template = get_email_template(
301 'code-import-status-updated.txt', app='code')
302 template_params = {
303 'body': make_email_body_for_code_import_update(
304 code_import, event, new_whiteboard),
305- 'branch': canonical_url(code_import.branch)}
306+ 'target': canonical_url(code_import.target)}
307
308 if person:
309 from_address = format_address(
310@@ -194,7 +194,7 @@
311 # Give the users a link to unsubscribe.
312 template_params['unsubscribe'] = (
313 "\nTo unsubscribe from this branch go to "
314- "%s/+edit-subscription." % canonical_url(branch))
315+ "%s/+edit-subscription." % canonical_url(target))
316 else:
317 template_params['unsubscribe'] = ''
318 for_person = subscription.person
319
320=== modified file 'lib/lp/code/mail/tests/test_codeimport.py'
321--- lib/lp/code/mail/tests/test_codeimport.py 2014-06-10 16:13:03 +0000
322+++ lib/lp/code/mail/tests/test_codeimport.py 2016-10-03 17:03:17 +0000
323@@ -1,4 +1,4 @@
324-# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
325+# Copyright 2010-2016 Canonical Ltd. This software is licensed under the
326 # GNU Affero General Public License version 3 (see the file LICENSE).
327
328 """Tests for code import related mailings"""
329@@ -7,7 +7,10 @@
330
331 import transaction
332
333-from lp.code.enums import RevisionControlSystems
334+from lp.code.enums import (
335+ RevisionControlSystems,
336+ TargetRevisionControlSystems,
337+ )
338 from lp.services.mail import stub
339 from lp.testing import (
340 login_person,
341@@ -21,8 +24,8 @@
342
343 layer = DatabaseFunctionalLayer
344
345- def test_cvs_import(self):
346- # Test the email for a new CVS import.
347+ def test_cvs_to_bzr_import(self):
348+ # Test the email for a new CVS-to-Bazaar import.
349 eric = self.factory.makePerson(name='eric')
350 fooix = self.factory.makeProduct(name='fooix')
351 # Eric needs to be logged in for the mail to be sent.
352@@ -44,8 +47,8 @@
353 '-- \nYou are getting this email because you are a member of the '
354 'vcs-imports team.\n', msg.get_payload(decode=True))
355
356- def test_svn_import(self):
357- # Test the email for a new subversion import.
358+ def test_svn_to_bzr_import(self):
359+ # Test the email for a new Subversion-to-Bazaar import.
360 eric = self.factory.makePerson(name='eric')
361 fooix = self.factory.makeProduct(name='fooix')
362 # Eric needs to be logged in for the mail to be sent.
363@@ -67,8 +70,8 @@
364 '-- \nYou are getting this email because you are a member of the '
365 'vcs-imports team.\n', msg.get_payload(decode=True))
366
367- def test_git_import(self):
368- # Test the email for a new git import.
369+ def test_git_to_bzr_import(self):
370+ # Test the email for a new git-to-Bazaar import.
371 eric = self.factory.makePerson(name='eric')
372 fooix = self.factory.makeProduct(name='fooix')
373 # Eric needs to be logged in for the mail to be sent.
374@@ -90,6 +93,30 @@
375 '-- \nYou are getting this email because you are a member of the '
376 'vcs-imports team.\n', msg.get_payload(decode=True))
377
378+ def test_git_to_git_import(self):
379+ # Test the email for a new git-to-git import.
380+ eric = self.factory.makePerson(name='eric')
381+ fooix = self.factory.makeProduct(name='fooix')
382+ # Eric needs to be logged in for the mail to be sent.
383+ login_person(eric)
384+ self.factory.makeProductCodeImport(
385+ git_repo_url='git://git.example.com/fooix.git',
386+ branch_name=u'master', product=fooix, registrant=eric,
387+ target_rcs_type=TargetRevisionControlSystems.GIT)
388+ transaction.commit()
389+ msg = message_from_string(stub.test_emails[0][2])
390+ self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
391+ self.assertEqual('~eric/fooix/+git/master', msg['X-Launchpad-Branch'])
392+ self.assertEqual(
393+ 'A new git code import has been requested '
394+ 'by Eric:\n'
395+ ' http://code.launchpad.dev/~eric/fooix/+git/master\n'
396+ 'from\n'
397+ ' git://git.example.com/fooix.git\n'
398+ '\n'
399+ '-- \nYou are getting this email because you are a member of the '
400+ 'vcs-imports team.\n', msg.get_payload(decode=True))
401+
402 def test_new_source_package_import(self):
403 # Test the email for a new sourcepackage import.
404 eric = self.factory.makePerson(name='eric')
405
406=== modified file 'lib/lp/code/model/codeimport.py'
407--- lib/lp/code/model/codeimport.py 2016-09-30 13:27:27 +0000
408+++ lib/lp/code/model/codeimport.py 2016-10-03 17:03:17 +0000
409@@ -26,7 +26,10 @@
410 Func,
411 Select,
412 )
413-from storm.locals import Store
414+from storm.locals import (
415+ Int,
416+ Store,
417+ )
418 from storm.references import Reference
419 from zope.component import getUtility
420 from zope.event import notify
421@@ -38,14 +41,17 @@
422 CodeImportJobState,
423 CodeImportResultStatus,
424 CodeImportReviewStatus,
425+ GitRepositoryType,
426 NON_CVS_RCS_TYPES,
427 RevisionControlSystems,
428+ TargetRevisionControlSystems,
429 )
430 from lp.code.errors import (
431 CodeImportAlreadyRequested,
432 CodeImportAlreadyRunning,
433 CodeImportNotInReviewedState,
434 )
435+from lp.code.interfaces.branch import IBranch
436 from lp.code.interfaces.branchtarget import IBranchTarget
437 from lp.code.interfaces.codeimport import (
438 ICodeImport,
439@@ -53,6 +59,8 @@
440 )
441 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
442 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
443+from lp.code.interfaces.gitnamespace import get_git_namespace
444+from lp.code.interfaces.gitrepository import IGitRepository
445 from lp.code.mail.codeimport import code_import_updated
446 from lp.code.model.codeimportjob import CodeImportJobWorkflow
447 from lp.code.model.codeimportresult import CodeImportResult
448@@ -71,8 +79,31 @@
449 _table = 'CodeImport'
450 _defaultOrder = ['id']
451
452+ def __init__(self, target=None, *args, **kwargs):
453+ if target is not None:
454+ assert 'branch' not in kwargs
455+ assert 'repository' not in kwargs
456+ if IBranch.providedBy(target):
457+ kwargs['branch'] = target
458+ elif IGitRepository.providedBy(target):
459+ kwargs['git_repository'] = target
460+ else:
461+ raise AssertionError("Unknown code import target %s" % target)
462+ super(CodeImport, self).__init__(*args, **kwargs)
463+
464 date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
465- branch = ForeignKey(dbName='branch', foreignKey='Branch', notNull=True)
466+ branch = ForeignKey(dbName='branch', foreignKey='Branch', notNull=False)
467+ git_repositoryID = Int(name='git_repository', allow_none=True)
468+ git_repository = Reference(git_repositoryID, 'GitRepository.id')
469+
470+ @property
471+ def target(self):
472+ if self.branch is not None:
473+ return self.branch
474+ else:
475+ assert self.git_repository is not None
476+ return self.git_repository
477+
478 registrant = ForeignKey(
479 dbName='registrant', foreignKey='Person',
480 storm_validator=validate_public_person, notNull=True)
481@@ -175,12 +206,14 @@
482 new_whiteboard = None
483 if 'whiteboard' in data:
484 whiteboard = data.pop('whiteboard')
485- if whiteboard != self.branch.whiteboard:
486- if whiteboard is None:
487- new_whiteboard = ''
488- else:
489- new_whiteboard = whiteboard
490- self.branch.whiteboard = whiteboard
491+ # XXX cjwatson 2016-10-03: Do we need something similar for Git?
492+ if self.branch is not None:
493+ if whiteboard != self.branch.whiteboard:
494+ if whiteboard is None:
495+ new_whiteboard = ''
496+ else:
497+ new_whiteboard = whiteboard
498+ self.branch.whiteboard = whiteboard
499 token = event_set.beginModify(self)
500 for name, value in data.items():
501 setattr(self, name, value)
502@@ -196,7 +229,7 @@
503 return event
504
505 def __repr__(self):
506- return "<CodeImport for %s>" % self.branch.unique_name
507+ return "<CodeImport for %s>" % self.target.unique_name
508
509 def tryFailingImportAgain(self, user):
510 """See `ICodeImport`."""
511@@ -233,7 +266,7 @@
512 class CodeImportSet:
513 """See `ICodeImportSet`."""
514
515- def new(self, registrant, context, branch_name, rcs_type,
516+ def new(self, registrant, context, branch_name, rcs_type, target_rcs_type,
517 url=None, cvs_root=None, cvs_module=None, review_status=None,
518 owner=None):
519 """See `ICodeImportSet`."""
520@@ -247,22 +280,37 @@
521 raise AssertionError(
522 "Don't know how to sanity check source details for unknown "
523 "rcs_type %s" % rcs_type)
524- target = IBranchTarget(context)
525+ if owner is None:
526+ owner = registrant
527+ if target_rcs_type == TargetRevisionControlSystems.BZR:
528+ target = IBranchTarget(context)
529+ namespace = target.getNamespace(owner)
530+ elif target_rcs_type == TargetRevisionControlSystems.GIT:
531+ if rcs_type != RevisionControlSystems.GIT:
532+ raise AssertionError(
533+ "Can't import rcs_type %s into a Git repository" %
534+ rcs_type)
535+ target = namespace = get_git_namespace(context, owner)
536+ else:
537+ raise AssertionError(
538+ "Can't import to target_rcs_type %s" % target_rcs_type)
539 if review_status is None:
540 # Auto approve imports.
541 review_status = CodeImportReviewStatus.REVIEWED
542 if not target.supports_code_imports:
543 raise AssertionError("%r doesn't support code imports" % target)
544- if owner is None:
545- owner = registrant
546 # Create the branch for the CodeImport.
547- namespace = target.getNamespace(owner)
548- import_branch = namespace.createBranch(
549- branch_type=BranchType.IMPORTED, name=branch_name,
550- registrant=registrant)
551+ if target_rcs_type == TargetRevisionControlSystems.BZR:
552+ import_target = namespace.createBranch(
553+ branch_type=BranchType.IMPORTED, name=branch_name,
554+ registrant=registrant)
555+ else:
556+ import_target = namespace.createRepository(
557+ repository_type=GitRepositoryType.IMPORTED, name=branch_name,
558+ registrant=registrant)
559
560 code_import = CodeImport(
561- registrant=registrant, owner=owner, branch=import_branch,
562+ registrant=registrant, owner=owner, target=import_target,
563 rcs_type=rcs_type, url=url,
564 cvs_root=cvs_root, cvs_module=cvs_module,
565 review_status=review_status)
566@@ -303,6 +351,9 @@
567 """See `ICodeImportSet`."""
568 return CodeImport.selectOneBy(branch=branch)
569
570+ def getByGitRepository(self, repository):
571+ return CodeImport.selectOneBy(git_repository=repository)
572+
573 def search(self, review_status=None, rcs_type=None):
574 """See `ICodeImportSet`."""
575 clauses = []
576
577=== modified file 'lib/lp/code/model/codeimportjob.py'
578--- lib/lp/code/model/codeimportjob.py 2015-07-08 16:05:11 +0000
579+++ lib/lp/code/model/codeimportjob.py 2016-10-03 17:03:17 +0000
580@@ -1,4 +1,4 @@
581-# Copyright 2009 Canonical Ltd. This software is licensed under the
582+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
583 # GNU Affero General Public License version 3 (see the file LICENSE).
584
585 """Database classes for the CodeImportJob table."""
586@@ -154,10 +154,10 @@
587 """See `ICodeImportJobWorkflow`."""
588 assert code_import.review_status == CodeImportReviewStatus.REVIEWED, (
589 "Review status of %s is not REVIEWED: %s" % (
590- code_import.branch.unique_name, code_import.review_status.name))
591+ code_import.target.unique_name, code_import.review_status.name))
592 assert code_import.import_job is None, (
593 "Already associated to a CodeImportJob: %s" % (
594- code_import.branch.unique_name))
595+ code_import.target.unique_name))
596
597 if interval is None:
598 interval = code_import.effective_update_interval
599@@ -182,13 +182,13 @@
600 """See `ICodeImportJobWorkflow`."""
601 assert code_import.review_status != CodeImportReviewStatus.REVIEWED, (
602 "The review status of %s is %s." % (
603- code_import.branch.unique_name, code_import.review_status.name))
604+ code_import.target.unique_name, code_import.review_status.name))
605 assert code_import.import_job is not None, (
606 "Not associated to a CodeImportJob: %s" % (
607- code_import.branch.unique_name,))
608+ code_import.target.unique_name,))
609 assert code_import.import_job.state == CodeImportJobState.PENDING, (
610 "The CodeImportJob associated to %s is %s." % (
611- code_import.branch.unique_name,
612+ code_import.target.unique_name,
613 code_import.import_job.state.name))
614 # CodeImportJobWorkflow is the only class that is allowed to delete
615 # CodeImportJob rows, so destroySelf is not exposed in ICodeImportJob.
616@@ -198,12 +198,12 @@
617 """See `ICodeImportJobWorkflow`."""
618 assert import_job.state == CodeImportJobState.PENDING, (
619 "The CodeImportJob associated with %s is %s."
620- % (import_job.code_import.branch.unique_name,
621+ % (import_job.code_import.target.unique_name,
622 import_job.state.name))
623 assert import_job.requesting_user is None, (
624 "The CodeImportJob associated with %s "
625 "was already requested by %s."
626- % (import_job.code_import.branch.unique_name,
627+ % (import_job.code_import.target.unique_name,
628 import_job.requesting_user.name))
629 # CodeImportJobWorkflow is the only class that is allowed to set the
630 # date_due and requesting_user attributes of CodeImportJob, they are
631@@ -219,7 +219,7 @@
632 """See `ICodeImportJobWorkflow`."""
633 assert import_job.state == CodeImportJobState.PENDING, (
634 "The CodeImportJob associated with %s is %s."
635- % (import_job.code_import.branch.unique_name,
636+ % (import_job.code_import.target.unique_name,
637 import_job.state.name))
638 assert machine.state == CodeImportMachineState.ONLINE, (
639 "The machine %s is %s."
640@@ -241,7 +241,7 @@
641 """See `ICodeImportJobWorkflow`."""
642 assert import_job.state == CodeImportJobState.RUNNING, (
643 "The CodeImportJob associated with %s is %s."
644- % (import_job.code_import.branch.unique_name,
645+ % (import_job.code_import.target.unique_name,
646 import_job.state.name))
647 # CodeImportJobWorkflow is the only class that is allowed to
648 # set the heartbeat and logtail attributes of CodeImportJob,
649@@ -281,7 +281,7 @@
650 """See `ICodeImportJobWorkflow`."""
651 assert import_job.state == CodeImportJobState.RUNNING, (
652 "The CodeImportJob associated with %s is %s."
653- % (import_job.code_import.branch.unique_name,
654+ % (import_job.code_import.target.unique_name,
655 import_job.state.name))
656 code_import = import_job.code_import
657 machine = import_job.machine
658@@ -311,7 +311,8 @@
659 naked_import.date_last_successful = result.date_created
660 # If the status was successful and revisions were imported, arrange
661 # for the branch to be mirrored.
662- if status == CodeImportResultStatus.SUCCESS:
663+ if (status == CodeImportResultStatus.SUCCESS and
664+ code_import.branch is not None):
665 code_import.branch.requestMirror()
666 getUtility(ICodeImportEventSet).newFinish(
667 code_import, machine)
668@@ -320,7 +321,7 @@
669 """See `ICodeImportJobWorkflow`."""
670 assert import_job.state == CodeImportJobState.RUNNING, (
671 "The CodeImportJob associated with %s is %s."
672- % (import_job.code_import.branch.unique_name,
673+ % (import_job.code_import.target.unique_name,
674 import_job.state.name))
675 # Cribbing from codeimport-job.txt, this method does four things:
676 # 1) deletes the passed in job,
677
678=== modified file 'lib/lp/code/model/gitnamespace.py'
679--- lib/lp/code/model/gitnamespace.py 2016-10-03 17:03:12 +0000
680+++ lib/lp/code/model/gitnamespace.py 2016-10-03 17:03:17 +0000
681@@ -1,4 +1,4 @@
682-# Copyright 2015 Canonical Ltd. This software is licensed under the
683+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
684 # GNU Affero General Public License version 3 (see the file LICENSE).
685
686 """Implementations of `IGitNamespace`."""
687@@ -238,6 +238,7 @@
688 has_defaults = False
689 allow_push_to_set_default = False
690 supports_merge_proposals = False
691+ supports_code_imports = False
692 allow_recipe_name_from_target = False
693
694 def __init__(self, person):
695@@ -316,6 +317,7 @@
696 has_defaults = True
697 allow_push_to_set_default = True
698 supports_merge_proposals = True
699+ supports_code_imports = True
700 allow_recipe_name_from_target = True
701
702 def __init__(self, person, project):
703@@ -401,6 +403,7 @@
704 has_defaults = True
705 allow_push_to_set_default = False
706 supports_merge_proposals = True
707+ supports_code_imports = True
708 allow_recipe_name_from_target = True
709
710 def __init__(self, person, distro_source_package):
711
712=== modified file 'lib/lp/code/model/hasbranches.py'
713--- lib/lp/code/model/hasbranches.py 2016-10-03 10:54:48 +0000
714+++ lib/lp/code/model/hasbranches.py 2016-10-03 17:03:17 +0000
715@@ -16,7 +16,10 @@
716 from zope.component import getUtility
717 from zope.security.proxy import removeSecurityProxy
718
719-from lp.code.enums import BranchMergeProposalStatus
720+from lp.code.enums import (
721+ BranchMergeProposalStatus,
722+ TargetRevisionControlSystems,
723+ )
724 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
725 from lp.code.interfaces.branchcollection import (
726 IAllBranches,
727@@ -130,5 +133,6 @@
728 owner=None):
729 """See `IHasCodeImports`."""
730 return getUtility(ICodeImportSet).new(
731- registrant, self, branch_name, rcs_type, url=url,
732+ registrant, self, branch_name, rcs_type,
733+ TargetRevisionControlSystems.BZR, url=url,
734 cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)
735
736=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
737--- lib/lp/code/model/tests/test_codeimport.py 2016-09-30 13:27:27 +0000
738+++ lib/lp/code/model/tests/test_codeimport.py 2016-10-03 17:03:17 +0000
739@@ -7,10 +7,15 @@
740 datetime,
741 timedelta,
742 )
743+from functools import partial
744
745 import pytz
746 from sqlobject import SQLObjectNotFound
747 from storm.store import Store
748+from testscenarios import (
749+ load_tests_apply_scenarios,
750+ WithScenarios,
751+ )
752 from zope.component import getUtility
753 from zope.security.proxy import removeSecurityProxy
754
755@@ -19,12 +24,14 @@
756 CodeImportResultStatus,
757 CodeImportReviewStatus,
758 RevisionControlSystems,
759+ TargetRevisionControlSystems,
760 )
761 from lp.code.errors import (
762 BranchCreatorNotMemberOfOwnerTeam,
763 CodeImportAlreadyRequested,
764 CodeImportAlreadyRunning,
765 CodeImportNotInReviewedState,
766+ GitRepositoryCreatorNotMemberOfOwnerTeam,
767 )
768 from lp.code.interfaces.branchtarget import IBranchTarget
769 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
770@@ -51,63 +58,100 @@
771 )
772
773
774-class TestCodeImportCreation(TestCaseWithFactory):
775+class TestCodeImportBase(WithScenarios, TestCaseWithFactory):
776+
777+ scenarios = [
778+ ("Branch", {
779+ "target_rcs_type": TargetRevisionControlSystems.BZR,
780+ "supports_source_cvs": True,
781+ "supports_source_svn": True,
782+ "supports_source_bzr": True,
783+ }),
784+ ("GitRepository", {
785+ "target_rcs_type": TargetRevisionControlSystems.GIT,
786+ "supports_source_cvs": False,
787+ "supports_source_svn": False,
788+ "supports_source_bzr": False,
789+ }),
790+ ]
791+
792+
793+class TestCodeImportCreation(TestCodeImportBase):
794 """Test the creation of CodeImports."""
795
796 layer = DatabaseFunctionalLayer
797
798 def test_new_svn_import_svn_scheme(self):
799 """A subversion import can use the svn:// scheme."""
800- code_import = CodeImportSet().new(
801+ create_func = partial(
802+ CodeImportSet().new,
803 registrant=self.factory.makePerson(),
804 context=self.factory.makeProduct(),
805- branch_name='imported',
806+ branch_name=u'imported',
807 rcs_type=RevisionControlSystems.BZR_SVN,
808+ target_rcs_type=self.target_rcs_type,
809 url=self.factory.getUniqueURL(scheme="svn"))
810- self.assertEqual(
811- CodeImportReviewStatus.REVIEWED,
812- code_import.review_status)
813- # No job is created for the import.
814- self.assertIsNot(None, code_import.import_job)
815+ if self.supports_source_svn:
816+ code_import = create_func()
817+ self.assertEqual(
818+ CodeImportReviewStatus.REVIEWED,
819+ code_import.review_status)
820+ # No job is created for the import.
821+ self.assertIsNot(None, code_import.import_job)
822+ else:
823+ self.assertRaises(AssertionError, create_func)
824
825 def test_reviewed_svn_import(self):
826 """A specific review status can be set for a new import."""
827- code_import = CodeImportSet().new(
828+ create_func = partial(
829+ CodeImportSet().new,
830 registrant=self.factory.makePerson(),
831 context=self.factory.makeProduct(),
832- branch_name='imported',
833+ branch_name=u'imported',
834 rcs_type=RevisionControlSystems.BZR_SVN,
835+ target_rcs_type=self.target_rcs_type,
836 url=self.factory.getUniqueURL(),
837 review_status=None)
838- self.assertEqual(
839- CodeImportReviewStatus.REVIEWED,
840- code_import.review_status)
841- # A job is created for the import.
842- self.assertIsNot(None, code_import.import_job)
843+ if self.supports_source_svn:
844+ code_import = create_func()
845+ self.assertEqual(
846+ CodeImportReviewStatus.REVIEWED,
847+ code_import.review_status)
848+ # A job is created for the import.
849+ self.assertIsNot(None, code_import.import_job)
850+ else:
851+ self.assertRaises(AssertionError, create_func)
852
853 def test_cvs_import_reviewed(self):
854 """A new CVS code import should have REVIEWED status."""
855- code_import = CodeImportSet().new(
856+ create_func = partial(
857+ CodeImportSet().new,
858 registrant=self.factory.makePerson(),
859 context=self.factory.makeProduct(),
860- branch_name='imported',
861+ branch_name=u'imported',
862 rcs_type=RevisionControlSystems.CVS,
863+ target_rcs_type=self.target_rcs_type,
864 cvs_root=self.factory.getUniqueURL(),
865- cvs_module='module',
866+ cvs_module=u'module',
867 review_status=None)
868- self.assertEqual(
869- CodeImportReviewStatus.REVIEWED,
870- code_import.review_status)
871- # A job is created for the import.
872- self.assertIsNot(None, code_import.import_job)
873+ if self.supports_source_cvs:
874+ code_import = create_func()
875+ self.assertEqual(
876+ CodeImportReviewStatus.REVIEWED,
877+ code_import.review_status)
878+ # A job is created for the import.
879+ self.assertIsNot(None, code_import.import_job)
880+ else:
881+ self.assertRaises(AssertionError, create_func)
882
883 def test_git_import_git_scheme(self):
884 """A git import can have a git:// style URL."""
885 code_import = CodeImportSet().new(
886 registrant=self.factory.makePerson(),
887 context=self.factory.makeProduct(),
888- branch_name='imported',
889+ branch_name=u'imported',
890 rcs_type=RevisionControlSystems.GIT,
891+ target_rcs_type=self.target_rcs_type,
892 url=self.factory.getUniqueURL(scheme="git"),
893 review_status=None)
894 self.assertEqual(
895@@ -121,8 +165,9 @@
896 code_import = CodeImportSet().new(
897 registrant=self.factory.makePerson(),
898 context=self.factory.makeProduct(),
899- branch_name='imported',
900+ branch_name=u'imported',
901 rcs_type=RevisionControlSystems.GIT,
902+ target_rcs_type=self.target_rcs_type,
903 url=self.factory.getUniqueURL(),
904 review_status=None)
905 self.assertEqual(
906@@ -133,18 +178,24 @@
907
908 def test_bzr_import_reviewed(self):
909 """A new bzr import is always reviewed by default."""
910- code_import = CodeImportSet().new(
911+ create_func = partial(
912+ CodeImportSet().new,
913 registrant=self.factory.makePerson(),
914 context=self.factory.makeProduct(),
915- branch_name='mirrored',
916+ branch_name=u'mirrored',
917 rcs_type=RevisionControlSystems.BZR,
918+ target_rcs_type=self.target_rcs_type,
919 url=self.factory.getUniqueURL(),
920 review_status=None)
921- self.assertEqual(
922- CodeImportReviewStatus.REVIEWED,
923- code_import.review_status)
924- # A job is created for the import.
925- self.assertIsNot(None, code_import.import_job)
926+ if self.supports_source_bzr:
927+ code_import = create_func()
928+ self.assertEqual(
929+ CodeImportReviewStatus.REVIEWED,
930+ code_import.review_status)
931+ # A job is created for the import.
932+ self.assertIsNot(None, code_import.import_job)
933+ else:
934+ self.assertRaises(AssertionError, create_func)
935
936 def test_junk_code_import_rejected(self):
937 """You are not allowed to create code imports targetting +junk."""
938@@ -152,8 +203,9 @@
939 self.assertRaises(AssertionError, CodeImportSet().new,
940 registrant=registrant,
941 context=registrant,
942- branch_name='imported',
943+ branch_name=u'imported',
944 rcs_type=RevisionControlSystems.GIT,
945+ target_rcs_type=self.target_rcs_type,
946 url=self.factory.getUniqueURL(),
947 review_status=None)
948
949@@ -161,19 +213,27 @@
950 """Test that we can create an import targetting a source package."""
951 registrant = self.factory.makePerson()
952 source_package = self.factory.makeSourcePackage()
953+ if self.target_rcs_type == TargetRevisionControlSystems.BZR:
954+ context = source_package
955+ else:
956+ context = source_package.distribution_sourcepackage
957 code_import = CodeImportSet().new(
958 registrant=registrant,
959- context=source_package,
960- branch_name='imported',
961+ context=context,
962+ branch_name=u'imported',
963 rcs_type=RevisionControlSystems.GIT,
964+ target_rcs_type=self.target_rcs_type,
965 url=self.factory.getUniqueURL(),
966 review_status=None)
967 code_import = removeSecurityProxy(code_import)
968 self.assertEqual(registrant, code_import.registrant)
969- self.assertEqual(registrant, code_import.branch.owner)
970- self.assertEqual(
971- IBranchTarget(source_package), code_import.branch.target)
972- self.assertEqual(source_package, code_import.branch.sourcepackage)
973+ if self.target_rcs_type == TargetRevisionControlSystems.BZR:
974+ self.assertEqual(registrant, code_import.branch.owner)
975+ self.assertEqual(IBranchTarget(context), code_import.branch.target)
976+ self.assertEqual(source_package, code_import.branch.sourcepackage)
977+ else:
978+ self.assertEqual(registrant, code_import.git_repository.owner)
979+ self.assertEqual(context, code_import.git_repository.target)
980 # And a job is still created
981 self.assertIsNot(None, code_import.import_job)
982
983@@ -183,20 +243,29 @@
984 owner = self.factory.makeTeam()
985 removeSecurityProxy(registrant).join(owner)
986 source_package = self.factory.makeSourcePackage()
987+ if self.target_rcs_type == TargetRevisionControlSystems.BZR:
988+ context = source_package
989+ else:
990+ context = source_package.distribution_sourcepackage
991 code_import = CodeImportSet().new(
992 registrant=registrant,
993- context=source_package,
994- branch_name='imported',
995+ context=context,
996+ branch_name=u'imported',
997 rcs_type=RevisionControlSystems.GIT,
998+ target_rcs_type=self.target_rcs_type,
999 url=self.factory.getUniqueURL(),
1000 review_status=None, owner=owner)
1001 code_import = removeSecurityProxy(code_import)
1002 self.assertEqual(registrant, code_import.registrant)
1003- self.assertEqual(owner, code_import.branch.owner)
1004- self.assertEqual(registrant, code_import.branch.registrant)
1005- self.assertEqual(
1006- IBranchTarget(source_package), code_import.branch.target)
1007- self.assertEqual(source_package, code_import.branch.sourcepackage)
1008+ if self.target_rcs_type == TargetRevisionControlSystems.BZR:
1009+ self.assertEqual(owner, code_import.branch.owner)
1010+ self.assertEqual(registrant, code_import.branch.registrant)
1011+ self.assertEqual(IBranchTarget(context), code_import.branch.target)
1012+ self.assertEqual(source_package, code_import.branch.sourcepackage)
1013+ else:
1014+ self.assertEqual(owner, code_import.git_repository.owner)
1015+ self.assertEqual(registrant, code_import.git_repository.registrant)
1016+ self.assertEqual(context, code_import.git_repository.target)
1017 # And a job is still created
1018 self.assertIsNot(None, code_import.import_job)
1019
1020@@ -205,30 +274,39 @@
1021 registrant = self.factory.makePerson()
1022 owner = self.factory.makeTeam()
1023 source_package = self.factory.makeSourcePackage()
1024+ if self.target_rcs_type == TargetRevisionControlSystems.BZR:
1025+ context = source_package
1026+ expected_exception = BranchCreatorNotMemberOfOwnerTeam
1027+ else:
1028+ context = source_package.distribution_sourcepackage
1029+ expected_exception = GitRepositoryCreatorNotMemberOfOwnerTeam
1030 self.assertRaises(
1031- BranchCreatorNotMemberOfOwnerTeam,
1032+ expected_exception,
1033 CodeImportSet().new,
1034 registrant=registrant,
1035- context=source_package,
1036- branch_name='imported',
1037+ context=context,
1038+ branch_name=u'imported',
1039 rcs_type=RevisionControlSystems.GIT,
1040+ target_rcs_type=self.target_rcs_type,
1041 url=self.factory.getUniqueURL(),
1042 review_status=None, owner=owner)
1043
1044
1045-class TestCodeImportDeletion(TestCaseWithFactory):
1046+class TestCodeImportDeletion(TestCodeImportBase):
1047 """Test the deletion of CodeImports."""
1048
1049 layer = LaunchpadFunctionalLayer
1050
1051 def test_delete(self):
1052 """Ensure CodeImport objects can be deleted via CodeImportSet."""
1053- code_import = self.factory.makeCodeImport()
1054+ code_import = self.factory.makeCodeImport(
1055+ target_rcs_type=self.target_rcs_type)
1056 CodeImportSet().delete(code_import)
1057
1058 def test_deleteIncludesJob(self):
1059 """Ensure deleting CodeImport objects deletes associated jobs."""
1060- code_import = self.factory.makeCodeImport()
1061+ code_import = self.factory.makeCodeImport(
1062+ target_rcs_type=self.target_rcs_type)
1063 login_person(getUtility(ILaunchpadCelebrities).vcs_imports.teamowner)
1064 job_id = code_import.import_job.id
1065 CodeImportJobSet().getById(job_id)
1066@@ -240,7 +318,10 @@
1067
1068 def test_deleteIncludesEvent(self):
1069 """Ensure deleting CodeImport objects deletes associated events."""
1070- code_import_event = self.factory.makeCodeImportEvent()
1071+ code_import = self.factory.makeCodeImport(
1072+ target_rcs_type=self.target_rcs_type)
1073+ code_import_event = self.factory.makeCodeImportEvent(
1074+ code_import=code_import)
1075 code_import_event_id = code_import_event.id
1076 CodeImportSet().delete(code_import_event.code_import)
1077 # CodeImportEvent.get should not raise anything.
1078@@ -251,7 +332,10 @@
1079
1080 def test_deleteIncludesResult(self):
1081 """Ensure deleting CodeImport objects deletes associated results."""
1082- code_import_result = self.factory.makeCodeImportResult()
1083+ code_import = self.factory.makeCodeImport(
1084+ target_rcs_type=self.target_rcs_type)
1085+ code_import_result = self.factory.makeCodeImportResult(
1086+ code_import=code_import)
1087 code_import_result_id = code_import_result.id
1088 CodeImportSet().delete(code_import_result.code_import)
1089 # CodeImportResult.get should not raise anything.
1090@@ -261,7 +345,7 @@
1091 SQLObjectNotFound, CodeImportResult.get, code_import_result_id)
1092
1093
1094-class TestCodeImportStatusUpdate(TestCaseWithFactory):
1095+class TestCodeImportStatusUpdate(TestCodeImportBase):
1096 """Test the status updates of CodeImports."""
1097
1098 layer = DatabaseFunctionalLayer
1099@@ -276,7 +360,8 @@
1100 job.destroySelf()
1101
1102 def makeApprovedImportWithPendingJob(self):
1103- code_import = self.factory.makeCodeImport()
1104+ code_import = self.factory.makeCodeImport(
1105+ target_rcs_type=self.target_rcs_type)
1106 code_import.updateFromData(
1107 {'review_status': CodeImportReviewStatus.REVIEWED},
1108 self.import_operator)
1109@@ -290,7 +375,8 @@
1110
1111 def test_approve(self):
1112 # Approving a code import will create a job for it.
1113- code_import = self.factory.makeCodeImport()
1114+ code_import = self.factory.makeCodeImport(
1115+ target_rcs_type=self.target_rcs_type)
1116 code_import.updateFromData(
1117 {'review_status': CodeImportReviewStatus.REVIEWED},
1118 self.import_operator)
1119@@ -300,7 +386,8 @@
1120
1121 def test_suspend_no_job(self):
1122 # Suspending a new import has no impact on jobs.
1123- code_import = self.factory.makeCodeImport()
1124+ code_import = self.factory.makeCodeImport(
1125+ target_rcs_type=self.target_rcs_type)
1126 code_import.updateFromData(
1127 {'review_status': CodeImportReviewStatus.SUSPENDED},
1128 self.import_operator)
1129@@ -330,7 +417,8 @@
1130
1131 def test_invalidate_no_job(self):
1132 # Invalidating a new import has no impact on jobs.
1133- code_import = self.factory.makeCodeImport()
1134+ code_import = self.factory.makeCodeImport(
1135+ target_rcs_type=self.target_rcs_type)
1136 code_import.updateFromData(
1137 {'review_status': CodeImportReviewStatus.INVALID},
1138 self.import_operator)
1139@@ -360,7 +448,8 @@
1140
1141 def test_markFailing_no_job(self):
1142 # Marking a new import as failing has no impact on jobs.
1143- code_import = self.factory.makeCodeImport()
1144+ code_import = self.factory.makeCodeImport(
1145+ target_rcs_type=self.target_rcs_type)
1146 code_import.updateFromData(
1147 {'review_status': CodeImportReviewStatus.FAILING},
1148 self.import_operator)
1149@@ -389,14 +478,15 @@
1150 CodeImportReviewStatus.FAILING, code_import.review_status)
1151
1152
1153-class TestCodeImportResultsAttribute(TestCaseWithFactory):
1154+class TestCodeImportResultsAttribute(TestCodeImportBase):
1155 """Test the results attribute of a CodeImport."""
1156
1157 layer = LaunchpadFunctionalLayer
1158
1159 def setUp(self):
1160 TestCaseWithFactory.setUp(self)
1161- self.code_import = self.factory.makeCodeImport()
1162+ self.code_import = self.factory.makeCodeImport(
1163+ target_rcs_type=self.target_rcs_type)
1164
1165 def tearDown(self):
1166 super(TestCodeImportResultsAttribute, self).tearDown()
1167@@ -454,7 +544,7 @@
1168 self.assertEqual(third, results[2])
1169
1170
1171-class TestConsecutiveFailureCount(TestCaseWithFactory):
1172+class TestConsecutiveFailureCount(TestCodeImportBase):
1173 """Tests for `ICodeImport.consecutive_failure_count`."""
1174
1175 layer = LaunchpadZopelessLayer
1176@@ -495,27 +585,31 @@
1177
1178 def test_consecutive_failure_count_zero_initially(self):
1179 # A new code import has a consecutive_failure_count of 0.
1180- code_import = self.factory.makeCodeImport()
1181+ code_import = self.factory.makeCodeImport(
1182+ target_rcs_type=self.target_rcs_type)
1183 self.assertEqual(0, code_import.consecutive_failure_count)
1184
1185 def test_consecutive_failure_count_succeed(self):
1186 # A code import that has succeeded once has a
1187 # consecutive_failure_count of 0.
1188- code_import = self.factory.makeCodeImport()
1189+ code_import = self.factory.makeCodeImport(
1190+ target_rcs_type=self.target_rcs_type)
1191 self.succeedImport(code_import)
1192 self.assertEqual(0, code_import.consecutive_failure_count)
1193
1194 def test_consecutive_failure_count_fail(self):
1195 # A code import that has failed once has a consecutive_failure_count
1196 # of 1.
1197- code_import = self.factory.makeCodeImport()
1198+ code_import = self.factory.makeCodeImport(
1199+ target_rcs_type=self.target_rcs_type)
1200 self.failImport(code_import)
1201 self.assertEqual(1, code_import.consecutive_failure_count)
1202
1203 def test_consecutive_failure_count_succeed_succeed_no_changes(self):
1204 # A code import that has succeeded then succeeded with no changes has
1205 # a consecutive_failure_count of 0.
1206- code_import = self.factory.makeCodeImport()
1207+ code_import = self.factory.makeCodeImport(
1208+ target_rcs_type=self.target_rcs_type)
1209 self.succeedImport(code_import)
1210 self.succeedImport(
1211 code_import, CodeImportResultStatus.SUCCESS_NOCHANGE)
1212@@ -524,7 +618,8 @@
1213 def test_consecutive_failure_count_succeed_succeed_partial(self):
1214 # A code import that has succeeded then succeeded with no changes has
1215 # a consecutive_failure_count of 0.
1216- code_import = self.factory.makeCodeImport()
1217+ code_import = self.factory.makeCodeImport(
1218+ target_rcs_type=self.target_rcs_type)
1219 self.succeedImport(code_import)
1220 self.succeedImport(
1221 code_import, CodeImportResultStatus.SUCCESS_NOCHANGE)
1222@@ -533,7 +628,8 @@
1223 def test_consecutive_failure_count_fail_fail(self):
1224 # A code import that has failed twice has a consecutive_failure_count
1225 # of 2.
1226- code_import = self.factory.makeCodeImport()
1227+ code_import = self.factory.makeCodeImport(
1228+ target_rcs_type=self.target_rcs_type)
1229 self.failImport(code_import)
1230 self.failImport(code_import)
1231 self.assertEqual(2, code_import.consecutive_failure_count)
1232@@ -541,7 +637,8 @@
1233 def test_consecutive_failure_count_fail_fail_succeed(self):
1234 # A code import that has failed twice then succeeded has a
1235 # consecutive_failure_count of 0.
1236- code_import = self.factory.makeCodeImport()
1237+ code_import = self.factory.makeCodeImport(
1238+ target_rcs_type=self.target_rcs_type)
1239 self.failImport(code_import)
1240 self.failImport(code_import)
1241 self.succeedImport(code_import)
1242@@ -550,7 +647,8 @@
1243 def test_consecutive_failure_count_fail_succeed_fail(self):
1244 # A code import that has failed then succeeded then failed again has a
1245 # consecutive_failure_count of 1.
1246- code_import = self.factory.makeCodeImport()
1247+ code_import = self.factory.makeCodeImport(
1248+ target_rcs_type=self.target_rcs_type)
1249 self.failImport(code_import)
1250 self.succeedImport(code_import)
1251 self.failImport(code_import)
1252@@ -559,7 +657,8 @@
1253 def test_consecutive_failure_count_succeed_fail_succeed(self):
1254 # A code import that has succeeded then failed then succeeded again
1255 # has a consecutive_failure_count of 0.
1256- code_import = self.factory.makeCodeImport()
1257+ code_import = self.factory.makeCodeImport(
1258+ target_rcs_type=self.target_rcs_type)
1259 self.succeedImport(code_import)
1260 self.failImport(code_import)
1261 self.succeedImport(code_import)
1262@@ -568,8 +667,10 @@
1263 def test_consecutive_failure_count_other_import_non_interference(self):
1264 # The failure or success of other code imports does not affect
1265 # consecutive_failure_count.
1266- code_import = self.factory.makeCodeImport()
1267- other_import = self.factory.makeCodeImport()
1268+ code_import = self.factory.makeCodeImport(
1269+ target_rcs_type=self.target_rcs_type)
1270+ other_import = self.factory.makeCodeImport(
1271+ target_rcs_type=self.target_rcs_type)
1272 self.failImport(code_import)
1273 self.assertEqual(1, code_import.consecutive_failure_count)
1274 self.failImport(other_import)
1275@@ -584,7 +685,7 @@
1276 self.assertEqual(1, code_import.consecutive_failure_count)
1277
1278
1279-class TestTryFailingImportAgain(TestCaseWithFactory):
1280+class TestTryFailingImportAgain(TestCodeImportBase):
1281 """Tests for `ICodeImport.tryFailingImportAgain`."""
1282
1283 layer = DatabaseFunctionalLayer
1284@@ -599,6 +700,7 @@
1285 outcomes = {}
1286 for status in CodeImportReviewStatus.items:
1287 code_import = self.factory.makeCodeImport(
1288+ target_rcs_type=self.target_rcs_type,
1289 review_status=CodeImportReviewStatus.NEW)
1290 code_import.updateFromData(
1291 {'review_status': status}, self.factory.makePerson())
1292@@ -619,7 +721,8 @@
1293 def test_resetsStatus(self):
1294 # tryFailingImportAgain sets the review_status of the import back to
1295 # REVIEWED.
1296- code_import = self.factory.makeCodeImport()
1297+ code_import = self.factory.makeCodeImport(
1298+ target_rcs_type=self.target_rcs_type)
1299 code_import.updateFromData(
1300 {'review_status': CodeImportReviewStatus.FAILING},
1301 self.factory.makePerson())
1302@@ -630,7 +733,8 @@
1303
1304 def test_requestsImport(self):
1305 # tryFailingImportAgain requests an import.
1306- code_import = self.factory.makeCodeImport()
1307+ code_import = self.factory.makeCodeImport(
1308+ target_rcs_type=self.target_rcs_type)
1309 code_import.updateFromData(
1310 {'review_status': CodeImportReviewStatus.FAILING},
1311 self.factory.makePerson())
1312@@ -640,7 +744,7 @@
1313 requester, code_import.import_job.requesting_user)
1314
1315
1316-class TestRequestImport(TestCaseWithFactory):
1317+class TestRequestImport(TestCodeImportBase):
1318 """Tests for `ICodeImport.requestImport`."""
1319
1320 layer = DatabaseFunctionalLayer
1321@@ -651,7 +755,8 @@
1322
1323 def test_requestsJob(self):
1324 code_import = self.factory.makeCodeImport(
1325- git_repo_url=self.factory.getUniqueURL())
1326+ git_repo_url=self.factory.getUniqueURL(),
1327+ target_rcs_type=self.target_rcs_type)
1328 requester = self.factory.makePerson()
1329 old_date = code_import.import_job.date_due
1330 code_import.requestImport(requester)
1331@@ -660,7 +765,8 @@
1332
1333 def test_noop_if_already_requested(self):
1334 code_import = self.factory.makeCodeImport(
1335- git_repo_url=self.factory.getUniqueURL())
1336+ git_repo_url=self.factory.getUniqueURL(),
1337+ target_rcs_type=self.target_rcs_type)
1338 requester = self.factory.makePerson()
1339 code_import.requestImport(requester)
1340 old_date = code_import.import_job.date_due
1341@@ -672,7 +778,8 @@
1342
1343 def test_optional_error_if_already_requested(self):
1344 code_import = self.factory.makeCodeImport(
1345- git_repo_url=self.factory.getUniqueURL())
1346+ git_repo_url=self.factory.getUniqueURL(),
1347+ target_rcs_type=self.target_rcs_type)
1348 requester = self.factory.makePerson()
1349 code_import.requestImport(requester)
1350 e = self.assertRaises(
1351@@ -681,10 +788,14 @@
1352 self.assertEqual(requester, e.requesting_user)
1353
1354 def test_exception_on_disabled(self):
1355- # get an SVN request which is suspended
1356+ # get an SVN/Git (as appropriate) request which is suspended
1357+ if self.supports_source_svn:
1358+ kwargs = {"svn_branch_url": self.factory.getUniqueURL()}
1359+ else:
1360+ kwargs = {"git_repo_url": self.factory.getUniqueURL()}
1361 code_import = self.factory.makeCodeImport(
1362- svn_branch_url=self.factory.getUniqueURL(),
1363- review_status=CodeImportReviewStatus.SUSPENDED)
1364+ target_rcs_type=self.target_rcs_type,
1365+ review_status=CodeImportReviewStatus.SUSPENDED, **kwargs)
1366 requester = self.factory.makePerson()
1367 # which leads to an exception if we try and ask for an import
1368 self.assertRaises(
1369@@ -693,10 +804,14 @@
1370
1371 def test_exception_if_already_running(self):
1372 code_import = self.factory.makeCodeImport(
1373- git_repo_url=self.factory.getUniqueURL())
1374+ git_repo_url=self.factory.getUniqueURL(),
1375+ target_rcs_type=self.target_rcs_type)
1376 code_import = make_running_import(factory=self.factory,
1377 code_import=code_import)
1378 requester = self.factory.makePerson()
1379 self.assertRaises(
1380 CodeImportAlreadyRunning, code_import.requestImport,
1381 requester)
1382+
1383+
1384+load_tests = load_tests_apply_scenarios
1385
1386=== modified file 'lib/lp/registry/browser/product.py'
1387--- lib/lp/registry/browser/product.py 2016-09-30 13:27:27 +0000
1388+++ lib/lp/registry/browser/product.py 2016-10-03 17:03:17 +0000
1389@@ -145,7 +145,10 @@
1390 from lp.code.browser.codeimport import validate_import_url
1391 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
1392 from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
1393-from lp.code.enums import RevisionControlSystems
1394+from lp.code.enums import (
1395+ RevisionControlSystems,
1396+ TargetRevisionControlSystems,
1397+ )
1398 from lp.code.errors import BranchExists
1399 from lp.code.interfaces.branch import IBranch
1400 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
1401@@ -1988,6 +1991,7 @@
1402 context=self.context,
1403 branch_name=branch_name,
1404 rcs_type=rcs_item,
1405+ target_rcs_type=TargetRevisionControlSystems.BZR,
1406 url=url,
1407 cvs_root=cvs_root,
1408 cvs_module=cvs_module)
1409
1410=== modified file 'lib/lp/testing/factory.py'
1411--- lib/lp/testing/factory.py 2016-10-03 17:03:12 +0000
1412+++ lib/lp/testing/factory.py 2016-10-03 17:03:17 +0000
1413@@ -114,6 +114,7 @@
1414 GitObjectType,
1415 GitRepositoryType,
1416 RevisionControlSystems,
1417+ TargetRevisionControlSystems,
1418 )
1419 from lp.code.errors import UnknownBranchTypeError
1420 from lp.code.interfaces.branch import IBranch
1421@@ -2374,21 +2375,28 @@
1422 cvs_module=None, context=None, branch_name=None,
1423 git_repo_url=None,
1424 bzr_branch_url=None, registrant=None,
1425- rcs_type=None, review_status=None):
1426+ rcs_type=None, target_rcs_type=None,
1427+ review_status=None):
1428 """Create and return a new, arbitrary code import.
1429
1430 The type of code import will be inferred from the source details
1431- passed in, but defaults to a Subversion import from an arbitrary
1432- unique URL.
1433+ passed in, but defaults to a Subversion->Bazaar import from an
1434+ arbitrary unique URL. (If the target type is specified as Git, then
1435+ the source type instead defaults to Git.)
1436 """
1437+ if target_rcs_type is None:
1438+ target_rcs_type = TargetRevisionControlSystems.BZR
1439 if (svn_branch_url is cvs_root is cvs_module is git_repo_url is
1440 bzr_branch_url is None):
1441- svn_branch_url = self.getUniqueURL()
1442+ if target_rcs_type == TargetRevisionControlSystems.BZR:
1443+ svn_branch_url = self.getUniqueURL()
1444+ else:
1445+ git_repo_url = self.getUniqueURL()
1446
1447 if context is None:
1448 context = self.makeProduct()
1449 if branch_name is None:
1450- branch_name = self.getUniqueString('name')
1451+ branch_name = self.getUniqueUnicode('name')
1452 if registrant is None:
1453 registrant = self.makePerson()
1454
1455@@ -2398,23 +2406,27 @@
1456 return code_import_set.new(
1457 registrant, context, branch_name,
1458 rcs_type=RevisionControlSystems.BZR_SVN,
1459+ target_rcs_type=target_rcs_type,
1460 url=svn_branch_url, review_status=review_status)
1461 elif git_repo_url is not None:
1462 assert rcs_type in (None, RevisionControlSystems.GIT)
1463 return code_import_set.new(
1464 registrant, context, branch_name,
1465 rcs_type=RevisionControlSystems.GIT,
1466+ target_rcs_type=target_rcs_type,
1467 url=git_repo_url, review_status=review_status)
1468 elif bzr_branch_url is not None:
1469 return code_import_set.new(
1470 registrant, context, branch_name,
1471 rcs_type=RevisionControlSystems.BZR,
1472+ target_rcs_type=target_rcs_type,
1473 url=bzr_branch_url, review_status=review_status)
1474 else:
1475 assert rcs_type in (None, RevisionControlSystems.CVS)
1476 return code_import_set.new(
1477 registrant, context, branch_name,
1478 rcs_type=RevisionControlSystems.CVS,
1479+ target_rcs_type=target_rcs_type,
1480 cvs_root=cvs_root, cvs_module=cvs_module,
1481 review_status=review_status)
1482
1483@@ -2440,9 +2452,10 @@
1484 changelog += entry
1485 return self.makeLibraryFileAlias(content=changelog.encode("utf-8"))
1486
1487- def makeCodeImportEvent(self):
1488+ def makeCodeImportEvent(self, code_import=None):
1489 """Create and return a CodeImportEvent."""
1490- code_import = self.makeCodeImport()
1491+ if code_import is None:
1492+ code_import = self.makeCodeImport()
1493 person = self.makePerson()
1494 code_import_event_set = getUtility(ICodeImportEventSet)
1495 return code_import_event_set.newCreate(code_import, person)