Merge lp:~jelmer/launchpad/bzr-code-imports-ui into lp:launchpad/db-devel

Proposed by Jelmer Vernooij
Status: Superseded
Proposed branch: lp:~jelmer/launchpad/bzr-code-imports-ui
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~jelmer/launchpad/bzr-code-imports
Diff against target: 720 lines (+88/-339)
11 files modified
lib/lp/code/browser/branch.py (+4/-47)
lib/lp/code/browser/codeimport.py (+23/-5)
lib/lp/code/browser/tests/test_branch.py (+3/-13)
lib/lp/code/enums.py (+2/-2)
lib/lp/code/stories/branches/xx-creating-branches.txt (+0/-179)
lib/lp/code/stories/codeimport/xx-create-codeimport.txt (+1/-0)
lib/lp/code/templates/branch-form-macros.pt (+0/-42)
lib/lp/code/templates/branch-import-details.pt (+6/-0)
lib/lp/code/templates/codeimport-new.pt (+14/-0)
lib/lp/registry/browser/productseries.py (+34/-50)
lib/lp/registry/templates/productseries-codesummary.pt (+1/-1)
To merge this branch: bzr merge lp:~jelmer/launchpad/bzr-code-imports-ui
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Michael Hudson-Doyle code Pending
Review via email: mp+65684@code.launchpad.net

This proposal has been superseded by a proposal from 2011-08-27.

Commit message

Remove the ability to register mirrored branches; instead, make it possible to register bzr code imports in the UI.

Description of the change

Make it possible to create new Bazaar code imports in the code import UI, disable the ability to create mirror branches.

This removes the user-visible distinction in the web UI between code imports (syncing from remote non-bzr branches) and mirrors (syncing from remote bzr branches), which should make the UI a bit easier to comprehend.

This doesn't migrate any of the existing mirror branches to be code imports, but that would be the next logical step.

It also doesn't remove the ability to create old-style mirrors via the API.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

+ bzr_branch_url = URIField(
...
+ allow_query=False, # Query makes no sense in Mercurial

:-)

Otherwise, well it looks fine. It's great to see so much code being deleted. I don't have the time to do a line by line review, and I don't have the Launchpad dev environment set up on my new laptop so I can't play around with it. I worry that there are probably more places that implicitly assume import branches are foreign in some way, but maybe not (also approximately noone uses mirror branches any more aiui).

It would be SO GREAT if we could get rid of the puller after this. I think this requires having some kind of token mechanism that grants the holder of the token the ability to read and write to a particular branch (and read any stacked on branches I guess) via the usual codehosting access. But that can wait :)

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Jelmer,

Thanks for trying to clean up this UI. It has always been a bit of a mess. I think your simplifications are good.

Twice in browser/codeimport.py we have a list of valid RCS types. It would be nice to define those one place rather than using the same set of literals twice such as:

@@ -576,7 +592,8 @@
151 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,
152 RevisionControlSystems.BZR_SVN,
153 RevisionControlSystems.GIT,
154 - RevisionControlSystems.HG):
155 + RevisionControlSystems.HG,
156 + RevisionControlSystems.BZR):

As with all UI changes, screenshot of before and after as well as clear instructions on how to exercise the changes makes for speedier review.

review: Approve
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Another direction the UI could go of course would be to not overly
distinguish between the various import types and just let the foreign
branch plugins figure it all out. This feels like it would make for a
simpler user experience, but it would also certainly be a big change. I
wonder if it might seem a bit too magical?

Cheers,
mwh

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On 26/06/11 23:26, Michael Hudson-Doyle wrote:
> Another direction the UI could go of course would be to not overly
> distinguish between the various import types and just let the foreign
> branch plugins figure it all out. This feels like it would make for a
> simpler user experience, but it would also certainly be a big change. I
> wonder if it might seem a bit too magical?
That's an interesting idea. "bzr branch" has that magic to figure out
what format the source branch is in too, and it seems to work fine
there. One way to do this would be to add a "AUTO" value to
RevisionControlSystems that simply tries the probers registered for each
of the CodeImportWorkers. That would (I think) be a minimal amount of
work, though it would of course only work for those importers that
actually have probers (BZR, BZR_SVN, GIT, HG).

CVS would still need to be special, but I guess there's no way around
that anyway, as it requires a root and a module to be specified.

Cheers,

Jelmer

Revision history for this message
Robert Collins (lifeless) wrote :

On Mon, Jun 27, 2011 at 11:04 AM, Jelmer Vernooij <email address hidden> wrote:
> CVS would still need to be special, but I guess there's no way around
> that anyway, as it requires a root and a module to be specified.

see config-manager for a url format for cvs that includes the root and module.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

On Mon, 27 Jun 2011 00:01:14 +0200, Jelmer Vernooij <email address hidden> wrote:
> On 26/06/11 23:26, Michael Hudson-Doyle wrote:
> > Another direction the UI could go of course would be to not overly
> > distinguish between the various import types and just let the foreign
> > branch plugins figure it all out. This feels like it would make for a
> > simpler user experience, but it would also certainly be a big change. I
> > wonder if it might seem a bit too magical?
> That's an interesting idea. "bzr branch" has that magic to figure out
> what format the source branch is in too, and it seems to work fine
> there. One way to do this would be to add a "AUTO" value to
> RevisionControlSystems that simply tries the probers registered for each
> of the CodeImportWorkers. That would (I think) be a minimal amount of
> work, though it would of course only work for those importers that
> actually have probers (BZR, BZR_SVN, GIT, HG).

Yeah, that would be pretty neat, although presenting bzr/svn/git/hg/auto
to the user wouldn't really be progress :)

> CVS would still need to be special, but I guess there's no way around
> that anyway, as it requires a root and a module to be specified.

Apparently config-manager defines a URL format for CVS we could borrow.
But that would be more useful for cleaning up the internals, I don't
think we could get away with expecting the user to know about it.

Cheers,
mwh

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Hi Brad,

On 24/06/11 20:15, Brad Crittenden wrote:
> Thanks for trying to clean up this UI. It has always been a bit of a mess. I think your simplifications are good.
>
> Twice in browser/codeimport.py we have a list of valid RCS types. It would be nice to define those one place rather than using the same set of literals twice such as:
>
> @@ -576,7 +592,8 @@
> 151 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,
> 152 RevisionControlSystems.BZR_SVN,
> 153 RevisionControlSystems.GIT,
> 154 - RevisionControlSystems.HG):
> 155 + RevisionControlSystems.HG,
> 156 + RevisionControlSystems.BZR):
Updated to use a single list.
>
>
> As with all UI changes, screenshot of before and after as well as clear instructions on how to exercise the changes makes for speedier review.
Thanks for the review! I'll make sure to include screenshots next time.

Cheers,

Jelmer

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On Sun, 2011-06-26 at 23:19 +0000, Robert Collins wrote:
> On Mon, Jun 27, 2011 at 11:04 AM, Jelmer Vernooij <email address hidden> wrote:
> > CVS would still need to be special, but I guess there's no way around
> > that anyway, as it requires a root and a module to be specified.
> see config-manager for a url format for cvs that includes the root and module.
Even if we do that, do we want users to have to know about that
particular format?

Cheers,

Jelmer

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

On Mon, 25 Jul 2011 13:21:44 -0000, Jelmer Vernooij <email address hidden> wrote:
> On Sun, 2011-06-26 at 23:19 +0000, Robert Collins wrote:
> > On Mon, Jun 27, 2011 at 11:04 AM, Jelmer Vernooij <email address hidden> wrote:
> > > CVS would still need to be special, but I guess there's no way around
> > > that anyway, as it requires a root and a module to be specified.
> > see config-manager for a url format for cvs that includes the root and module.
> Even if we do that, do we want users to have to know about that
> particular format?

No.

Cheers,
mwh

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2011-08-24 11:52:43 +0000
+++ lib/lp/code/browser/branch.py 2011-08-25 10:47:30 +0000
@@ -1120,15 +1120,12 @@
11201120
1121 class schema(Interface):1121 class schema(Interface):
1122 use_template(1122 use_template(
1123 IBranch, include=['owner', 'name', 'url', 'lifecycle_status'])1123 IBranch, include=['owner', 'name', 'lifecycle_status'])
1124 branch_type = copy_field(
1125 IBranch['branch_type'], vocabulary=UICreatableBranchType)
11261124
1127 for_input = True1125 for_input = True
1128 field_names = ['owner', 'name', 'branch_type', 'url', 'lifecycle_status']1126 field_names = ['owner', 'name', 'lifecycle_status']
11291127
1130 branch = None1128 branch = None
1131 custom_widget('branch_type', LaunchpadRadioWidgetWithDescription)
1132 custom_widget('lifecycle_status', LaunchpadRadioWidgetWithDescription)1129 custom_widget('lifecycle_status', LaunchpadRadioWidgetWithDescription)
11331130
1134 initial_focus_widget = 'name'1131 initial_focus_widget = 'name'
@@ -1157,27 +1154,17 @@
1157 """1154 """
1158 return IPerson(self.context, self.user)1155 return IPerson(self.context, self.user)
11591156
1160 def showOptionalMarker(self, field_name):
1161 """Don't show the optional marker for url."""
1162 if field_name == 'url':
1163 return False
1164 else:
1165 return LaunchpadFormView.showOptionalMarker(self, field_name)
1166
1167 @action('Register Branch', name='add')1157 @action('Register Branch', name='add')
1168 def add_action(self, action, data):1158 def add_action(self, action, data):
1169 """Handle a request to create a new branch for this product."""1159 """Handle a request to create a new branch for this product."""
1170 try:1160 try:
1171 ui_branch_type = data['branch_type']
1172 namespace = self.target.getNamespace(data['owner'])1161 namespace = self.target.getNamespace(data['owner'])
1173 self.branch = namespace.createBranch(1162 self.branch = namespace.createBranch(
1174 branch_type=BranchType.items[ui_branch_type.name],1163 branch_type=BranchType.HOSTED,
1175 name=data['name'],1164 name=data['name'],
1176 registrant=self.user,1165 registrant=self.user,
1177 url=data.get('url'),1166 url=None,
1178 lifecycle_status=data['lifecycle_status'])1167 lifecycle_status=data['lifecycle_status'])
1179 if self.branch.branch_type == BranchType.MIRRORED:
1180 self.branch.requestMirror()
1181 except BranchCreationForbidden:1168 except BranchCreationForbidden:
1182 self.addError(1169 self.addError(
1183 "You are not allowed to create branches in %s." %1170 "You are not allowed to create branches in %s." %
@@ -1195,36 +1182,6 @@
1195 'owner',1182 'owner',
1196 'You are not a member of %s' % owner.displayname)1183 'You are not a member of %s' % owner.displayname)
11971184
1198 branch_type = data.get('branch_type')
1199 # If branch_type failed to validate, then the rest of the method
1200 # doesn't make any sense.
1201 if branch_type is None:
1202 return
1203
1204 # If the branch is a MIRRORED branch, then the url
1205 # must be supplied, and if HOSTED the url must *not*
1206 # be supplied.
1207 url = data.get('url')
1208 if branch_type == UICreatableBranchType.MIRRORED:
1209 if url is None:
1210 # If the url is not set due to url validation errors,
1211 # there will be an error set for it.
1212 error = self.getFieldError('url')
1213 if not error:
1214 self.setFieldError(
1215 'url',
1216 'Branch URLs are required for Mirrored branches.')
1217 elif branch_type == UICreatableBranchType.HOSTED:
1218 if url is not None:
1219 self.setFieldError(
1220 'url',
1221 'Branch URLs cannot be set for Hosted branches.')
1222 elif branch_type == UICreatableBranchType.REMOTE:
1223 # A remote location can, but doesn't have to be set.
1224 pass
1225 else:
1226 raise AssertionError('Unknown branch type')
1227
1228 @property1185 @property
1229 def cancel_url(self):1186 def cancel_url(self):
1230 return canonical_url(self.context)1187 return canonical_url(self.context)
12311188
=== modified file 'lib/lp/code/browser/codeimport.py'
--- lib/lp/code/browser/codeimport.py 2011-05-27 21:12:25 +0000
+++ lib/lp/code/browser/codeimport.py 2011-08-25 10:47:30 +0000
@@ -271,6 +271,16 @@
271 allow_fragment=False, # Fragment makes no sense in Mercurial271 allow_fragment=False, # Fragment makes no sense in Mercurial
272 trailing_slash=False) # See http://launchpad.net/bugs/56357.272 trailing_slash=False) # See http://launchpad.net/bugs/56357.
273273
274 bzr_branch_url = URIField(
275 title=_("Bazaar branch URL"), required=False,
276 description=_("The URL of the Bazaar branch."),
277 allowed_schemes=["http", "https", "bzr"],
278 allow_userinfo=False, # Only anonymous access is supported.
279 allow_port=True,
280 allow_query=False, # Query makes no sense in Mercurial
281 allow_fragment=False, # Fragment makes no sense in Mercurial
282 trailing_slash=False)
283
274 branch_name = copy_field(284 branch_name = copy_field(
275 IBranch['name'],285 IBranch['name'],
276 __name__='branch_name',286 __name__='branch_name',
@@ -346,9 +356,9 @@
346 # display them separately in the form.356 # display them separately in the form.
347 soup = BeautifulSoup(self.widgets['rcs_type']())357 soup = BeautifulSoup(self.widgets['rcs_type']())
348 fields = soup.findAll('input')358 fields = soup.findAll('input')
349 [cvs_button, svn_button, git_button, hg_button, empty_marker] = [359 [cvs_button, svn_button, git_button, hg_button, bzr_button,
350 field for field in fields360 empty_marker] = [field for field in fields
351 if field.get('value') in ['CVS', 'BZR_SVN', 'GIT', 'HG', '1']]361 if field.get('value') in ['CVS', 'BZR_SVN', 'GIT', 'HG', 'BZR', '1']]
352 cvs_button['onclick'] = 'updateWidgets()'362 cvs_button['onclick'] = 'updateWidgets()'
353 svn_button['onclick'] = 'updateWidgets()'363 svn_button['onclick'] = 'updateWidgets()'
354 git_button['onclick'] = 'updateWidgets()'364 git_button['onclick'] = 'updateWidgets()'
@@ -358,6 +368,7 @@
358 self.rcs_type_svn = str(svn_button)368 self.rcs_type_svn = str(svn_button)
359 self.rcs_type_git = str(git_button)369 self.rcs_type_git = str(git_button)
360 self.rcs_type_hg = str(hg_button)370 self.rcs_type_hg = str(hg_button)
371 self.rcs_type_bzr = str(bzr_button)
361 self.rcs_type_emptymarker = str(empty_marker)372 self.rcs_type_emptymarker = str(empty_marker)
362373
363 def _getImportLocation(self, data):374 def _getImportLocation(self, data):
@@ -371,6 +382,8 @@
371 return None, None, data.get('git_repo_url')382 return None, None, data.get('git_repo_url')
372 elif rcs_type == RevisionControlSystems.HG:383 elif rcs_type == RevisionControlSystems.HG:
373 return None, None, data.get('hg_repo_url')384 return None, None, data.get('hg_repo_url')
385 elif rcs_type == RevisionControlSystems.BZR:
386 return None, None, data.get('bzr_branch_url')
374 else:387 else:
375 raise AssertionError(388 raise AssertionError(
376 'Unexpected revision control type %r.' % rcs_type)389 'Unexpected revision control type %r.' % rcs_type)
@@ -485,6 +498,9 @@
485 elif rcs_type == RevisionControlSystems.HG:498 elif rcs_type == RevisionControlSystems.HG:
486 self._validateURL(499 self._validateURL(
487 data.get('hg_repo_url'), field_name='hg_repo_url')500 data.get('hg_repo_url'), field_name='hg_repo_url')
501 elif rcs_type == RevisionControlSystems.BZR:
502 self._validateURL(
503 data.get('bzr_branch_url'), field_name='bzr_branch_url')
488 else:504 else:
489 raise AssertionError(505 raise AssertionError(
490 'Unexpected revision control type %r.' % rcs_type)506 'Unexpected revision control type %r.' % rcs_type)
@@ -576,7 +592,8 @@
576 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,592 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,
577 RevisionControlSystems.BZR_SVN,593 RevisionControlSystems.BZR_SVN,
578 RevisionControlSystems.GIT,594 RevisionControlSystems.GIT,
579 RevisionControlSystems.HG):595 RevisionControlSystems.HG,
596 RevisionControlSystems.BZR):
580 self.form_fields = self.form_fields.omit(597 self.form_fields = self.form_fields.omit(
581 'cvs_root', 'cvs_module')598 'cvs_root', 'cvs_module')
582 else:599 else:
@@ -611,7 +628,8 @@
611 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,628 elif self.code_import.rcs_type in (RevisionControlSystems.SVN,
612 RevisionControlSystems.BZR_SVN,629 RevisionControlSystems.BZR_SVN,
613 RevisionControlSystems.GIT,630 RevisionControlSystems.GIT,
614 RevisionControlSystems.HG):631 RevisionControlSystems.HG,
632 RevisionControlSystems.BZR):
615 self._validateURL(data.get('url'), self.code_import)633 self._validateURL(data.get('url'), self.code_import)
616 else:634 else:
617 raise AssertionError('Unknown rcs_type for code import.')635 raise AssertionError('Unknown rcs_type for code import.')
618636
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2011-08-22 18:10:47 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2011-08-25 10:47:30 +0000
@@ -191,8 +191,8 @@
191 "This is a short error message.",191 "This is a short error message.",
192 branch_view.mirror_status_message)192 branch_view.mirror_status_message)
193193
194 def testBranchAddRequestsMirror(self):194 def testBranchAddRequests(self):
195 """Registering a mirrored branch requests a mirror."""195 """Registering a branch that requests a mirror."""
196 arbitrary_person = self.factory.makePerson()196 arbitrary_person = self.factory.makePerson()
197 arbitrary_product = self.factory.makeProduct()197 arbitrary_product = self.factory.makeProduct()
198 login_person(arbitrary_person)198 login_person(arbitrary_person)
@@ -200,9 +200,8 @@
200 add_view = BranchAddView(arbitrary_person, self.request)200 add_view = BranchAddView(arbitrary_person, self.request)
201 add_view.initialize()201 add_view.initialize()
202 data = {202 data = {
203 'branch_type': BranchType.MIRRORED,203 'branch_type': BranchType.HOSTED,
204 'name': 'some-branch',204 'name': 'some-branch',
205 'url': 'http://example.com',
206 'title': 'Branch Title',205 'title': 'Branch Title',
207 'summary': '',206 'summary': '',
208 'lifecycle_status': BranchLifecycleStatus.DEVELOPMENT,207 'lifecycle_status': BranchLifecycleStatus.DEVELOPMENT,
@@ -212,15 +211,6 @@
212 'product': arbitrary_product,211 'product': arbitrary_product,
213 }212 }
214 add_view.add_action.success(data)213 add_view.add_action.success(data)
215 # Make sure that next_mirror_time is a datetime, not an sqlbuilder
216 # expression.
217 removeSecurityProxy(add_view.branch).sync()
218 now = datetime.now(pytz.timezone('UTC'))
219 self.assertNotEqual(None, add_view.branch.next_mirror_time)
220 self.assertTrue(
221 add_view.branch.next_mirror_time < now,
222 "next_mirror_time not set to UTC_NOW: %s < %s"
223 % (add_view.branch.next_mirror_time, now))
224 finally:214 finally:
225 logout()215 logout()
226216
227217
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py 2011-08-25 10:47:05 +0000
+++ lib/lp/code/enums.py 2011-08-25 10:47:30 +0000
@@ -99,8 +99,8 @@
99 IMPORTED = DBItem(3, """99 IMPORTED = DBItem(3, """
100 Imported100 Imported
101101
102 Branches that have been converted from some other revision102 Branches that have been imported from an externally hosted
103 control system into bzr and are made available through Launchpad.103 branch in bzr or another VCS and are made available through Launchpad.
104 """)104 """)
105105
106 REMOTE = DBItem(4, """106 REMOTE = DBItem(4, """
107107
=== modified file 'lib/lp/code/stories/branches/xx-creating-branches.txt'
--- lib/lp/code/stories/branches/xx-creating-branches.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/code/stories/branches/xx-creating-branches.txt 2011-08-25 10:47:30 +0000
@@ -32,94 +32,8 @@
32 >>> browser.open('http://code.launchpad.dev/redfish')32 >>> browser.open('http://code.launchpad.dev/redfish')
33 >>> browser.getLink("Register a branch").click()33 >>> browser.getLink("Register a branch").click()
3434
35 >>> browser.getControl('Hosted').click()
36 >>> browser.getControl('Name').value = 'hosted-branch'35 >>> browser.getControl('Name').value = 'hosted-branch'
3736
38Branch URLs are not valid for hosted branches.
39
40 >>> browser.getControl('Branch URL').value = (
41 ... 'http://example.com/hosted-branch')
42 >>> browser.getControl('Register Branch').click()
43 >>> for message in get_feedback_messages(browser.contents):
44 ... print message
45 There is 1 error.
46 Branch URLs cannot be set for Hosted branches.
47
48 >>> browser.getControl('Branch URL').value = ''
49 >>> browser.getControl('Register Branch').click()
50
51 >>> def print_branch_info(browser):
52 ... branch_info = find_tag_by_id(browser.contents, 'branch-info')
53 ... print extract_text(branch_info)
54 >>> print_branch_info(browser)
55 Branch information
56 Owner: Sample Person
57 Project: Redfish
58 Status: Development
59
60
61Creating a mirrored branch
62--------------------------
63
64Mirrored branches are primarily hosted outside of Launchpad, and
65Launchpad mirrors the branch.
66
67 >>> browser.open('http://code.launchpad.dev/redfish')
68 >>> browser.getLink("Register a branch").click()
69
70 >>> print_radio_button_field(browser.contents, 'branch_type')
71 ( ) Hosted
72 (*) Mirrored
73 ( ) Remote
74
75 >>> browser.getControl('Name').value = 'mirrored-branch'
76
77Branch URLs are required for mirrored branches.
78
79 >>> browser.getControl('Register Branch').click()
80 >>> for message in get_feedback_messages(browser.contents):
81 ... print message
82 There is 1 error.
83 Branch URLs are required for Mirrored branches.
84
85 >>> browser.getControl('Branch URL').value = (
86 ... 'http://example.com/mirrored-branch')
87 >>> browser.getControl('Register Branch').click()
88
89 >>> print_branch_info(browser)
90 Branch information
91 Owner: Sample Person
92 Project: Redfish
93 Status: Development
94 Location: http://example.com/mirrored-branch
95 Last mirrored: Not mirrored yet
96 Next mirror: As soon as possible
97
98
99Creating a remote branch
100------------------------
101
102The contents of Remote branches are not accessible through Launchpad.
103
104 >>> browser.open('http://code.launchpad.dev/redfish')
105 >>> browser.getLink("Register a branch").click()
106
107 >>> browser.getControl('Remote').click()
108 >>> browser.getControl('Name').value = 'remote-branch'
109
110A remote branch may have a URL, but doesn't have to.
111
112 >>> browser.getControl('Branch URL').value = (
113 ... 'http://example.com/remote-branch')
114 >>> browser.getControl('Register Branch').click()
115 >>> print_branch_info(browser)
116 Branch information
117 Owner: Sample Person
118 Project: Redfish
119 Status: Development
120 Location: http://example.com/remote-branch
121
122
123Finding product/+addbranch37Finding product/+addbranch
124--------------------------38--------------------------
12539
@@ -209,34 +123,6 @@
209 Other branches owned by No Privileges Person123 Other branches owned by No Privileges Person
210124
211125
212Adding a branch via a product series
213------------------------------------
214
215A user can register a branch from the project series page using the
216'Submit code' link, or in the case of a series driver, by using the
217'registering a mirrored branch' link.
218
219 >>> browser.open('http://launchpad.dev/gnome-terminal/trunk')
220 >>> browser.getLink('registering a mirrored branch').click()
221 >>> print browser.title
222 Register a branch : Code : GNOME Terminal
223
224The user sees that he is registering a branch for the series' project.
225
226 >>> print extract_text(find_main_content(browser.contents).h1)
227 Register a branch on GNOME Terminal
228
229After posting the form, he sees the registered branch page.
230
231 >>> browser.getControl('Branch URL').value = (
232 ... 'http://example.com/applets/master')
233 >>> browser.getControl('Name').value = 'master'
234 >>> browser.getControl('Experimental').click()
235 >>> browser.getControl('Register Branch').click()
236 >>> print browser.title
237 master : Code : GNOME Terminal
238
239
240Finding person/+addbranch126Finding person/+addbranch
241-------------------------127-------------------------
242128
@@ -271,7 +157,6 @@
271 Ubuntu Gnome Team (name18)157 Ubuntu Gnome Team (name18)
272 Warty Security Team (name20)158 Warty Security Team (name20)
273159
274
275Conflict on unique name160Conflict on unique name
276-----------------------161-----------------------
277162
@@ -288,8 +173,6 @@
288Try a adding a conflicting branch from the product/+addbranch form.173Try a adding a conflicting branch from the product/+addbranch form.
289174
290 >>> browser.open('http://code.launchpad.dev/gnome-terminal/+addbranch')175 >>> browser.open('http://code.launchpad.dev/gnome-terminal/+addbranch')
291 >>> browser.getControl('Branch URL').value = (
292 ... 'http://example.com/gnome-terminal/main-dup')
293176
294Trying to post the form without filling a name at all should not cause177Trying to post the form without filling a name at all should not cause
295an oops!178an oops!
@@ -321,7 +204,6 @@
321 >>> browser.getControl('Owner').displayValue = [204 >>> browser.getControl('Owner').displayValue = [
322 ... 'Landscape Developers']205 ... 'Landscape Developers']
323 >>> browser.getControl('Name').value = 'main'206 >>> browser.getControl('Name').value = 'main'
324 >>> browser.getControl('Hosted').click()
325 >>> browser.getControl('Register Branch').click()207 >>> browser.getControl('Register Branch').click()
326 >>> print browser.url208 >>> print browser.url
327 http://code.launchpad.dev/~landscape-developers/gnome-terminal/main209 http://code.launchpad.dev/~landscape-developers/gnome-terminal/main
@@ -335,7 +217,6 @@
335 >>> browser.getControl('Owner').displayValue = [217 >>> browser.getControl('Owner').displayValue = [
336 ... 'Landscape Developers']218 ... 'Landscape Developers']
337 >>> browser.getControl('Name').value = 'main'219 >>> browser.getControl('Name').value = 'main'
338 >>> browser.getControl('Hosted').click()
339 >>> browser.getControl('Register Branch').click()220 >>> browser.getControl('Register Branch').click()
340 >>> for message in get_feedback_messages(browser.contents):221 >>> for message in get_feedback_messages(browser.contents):
341 ... print extract_text(message)222 ... print extract_text(message)
@@ -343,65 +224,6 @@
343 Landscape Developers already has a branch for GNOME Terminal called main.224 Landscape Developers already has a branch for GNOME Terminal called main.
344225
345226
346Checking URLs
347-------------
348
349URL validation should check that the entered URL is not the root of a
350site.
351
352 >>> user_browser.open('http://code.launchpad.dev/applets')
353 >>> user_browser.getLink("Register a branch").click()
354 >>> user_browser.getControl('Branch URL').value = 'http://example.com'
355 >>> user_browser.getControl('Name').value = 'unique-name'
356 >>> user_browser.getControl('Register Branch').click()
357 >>> messages = find_tags_by_class(user_browser.contents, 'message')
358 >>> for element in messages:
359 ... print element.renderContents()
360 There is 1 error.
361 URLs for branches cannot point to the root of a site.
362
363URL validation should check that the entered URL is not from Launchpad.
364
365 >>> user_browser.open('http://code.launchpad.dev/applets')
366 >>> user_browser.getLink("Register a branch").click()
367 >>> user_browser.getControl('Branch URL').value = (
368 ... 'http://code.launchpad.dev/~testuser/')
369 >>> user_browser.getControl('Name').value = 'unique-name'
370 >>> user_browser.getControl('Register Branch').click()
371 >>> for message in get_feedback_messages(user_browser.contents):
372 ... print message
373 There is 1 error.
374 For Launchpad to mirror a branch, the original branch cannot be on
375 launchpad.dev.
376
377As well as checking against the root site set in the config, a check is
378also done against the value stored as a database constraint.
379
380 >>> user_browser.open('http://code.launchpad.dev/applets')
381 >>> user_browser.getLink("Register a branch").click()
382 >>> user_browser.getControl('Branch URL').value = (
383 ... 'http://bazaar.launchpad.net/foo/bar/')
384 >>> user_browser.getControl('Name').value = 'unique-name'
385 >>> user_browser.getControl('Register Branch').click()
386 >>> for message in get_feedback_messages(user_browser.contents):
387 ... print message
388 There is 1 error.
389 For Launchpad to mirror a branch, the original branch cannot be on
390 http://bazaar.launchpad.net.
391
392Trailing slashes on URLs are removed.
393
394 >>> user_browser.getControl('Branch URL').value = (
395 ... 'sftp://example.com/~lifeless/pyresources/')
396 >>> user_browser.getControl('Register Branch').click()
397 >>> print_tag_with_id(user_browser.contents, 'branch-info')
398 Branch information
399 Owner: ...
400 ...
401 Location: sftp://example.com/~lifeless/pyresources
402 ...
403
404
405Attempting to create a branch in a forbidden project227Attempting to create a branch in a forbidden project
406----------------------------------------------------228----------------------------------------------------
407229
@@ -423,7 +245,6 @@
423245
424 >>> user_browser.open('http://code.launchpad.dev/landscape')246 >>> user_browser.open('http://code.launchpad.dev/landscape')
425 >>> user_browser.getLink("Register a branch").click()247 >>> user_browser.getLink("Register a branch").click()
426 >>> user_browser.getControl('Branch URL').value = 'http://foo.com/bar'
427 >>> user_browser.getControl('Name').value = 'landscape1'248 >>> user_browser.getControl('Name').value = 'landscape1'
428 >>> user_browser.getControl('Register Branch').click()249 >>> user_browser.getControl('Register Branch').click()
429 >>> messages = find_tags_by_class(user_browser.contents, 'message')250 >>> messages = find_tags_by_class(user_browser.contents, 'message')
430251
=== modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
--- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2010-09-28 19:25:54 +0000
+++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2011-08-25 10:47:30 +0000
@@ -56,6 +56,7 @@
56The default foreign VCS type is Subversion.56The default foreign VCS type is Subversion.
5757
58 >>> print_radio_button_field(browser.contents, "rcs_type")58 >>> print_radio_button_field(browser.contents, "rcs_type")
59 ( ) Bazaar
59 ( ) Git60 ( ) Git
60 ( ) Mercurial61 ( ) Mercurial
61 (*) Subversion62 (*) Subversion
6263
=== modified file 'lib/lp/code/templates/branch-form-macros.pt'
--- lib/lp/code/templates/branch-form-macros.pt 2009-10-02 15:02:23 +0000
+++ lib/lp/code/templates/branch-form-macros.pt 2011-08-25 10:47:30 +0000
@@ -17,20 +17,6 @@
1717
18<script type="text/javascript">18<script type="text/javascript">
19//<![CDATA[19//<![CDATA[
20function update_branch_url()
21{
22 var branch_type_field = document.getElementsByName('field.branch_type')
23 var form = branch_type_field[0].form;
24 var branch_type = 'None';
25 for (var i = 0; i < branch_type_field.length; i++) {
26 if (branch_type_field[i].checked) {
27 branch_type = branch_type_field[i].value;
28 break;
29 }
30 }
31 updateField(form['field.url'], branch_type != 'HOSTED');
32}
33
34function update_branch_unique_name()20function update_branch_unique_name()
35{21{
36 var unique_name = document.getElementById("branch-unique-name")22 var unique_name = document.getElementById("branch-unique-name")
@@ -43,44 +29,16 @@
43 unique_name.innerHTML = branch_name29 unique_name.innerHTML = branch_name
44}30}
4531
46function populate_branch_name_from_url()
47{
48 url_field = document.getElementById('field.url');
49 var url_text = trim(url_field.value);
50 // strip of any trailing slashes
51 url_text = url_text.replace(/\/+$/, '')
52 if (url_text != url_field.value) {
53 url_field.value = url_text;
54 }
55 var name_field = document.getElementById('field.name');
56 if (name_field.value == '')
57 {
58 // parse the value of the url field
59 url_bits = url_text.split('/');
60 if (url_bits.length > 2) {
61 // attempt at not barfing on something completely invalid
62 last_bit = url_bits[url_bits.length - 1];
63 name_field.value = last_bit;
64 }
65 }
66}
67
68function hookUpBranchFieldFunctions()32function hookUpBranchFieldFunctions()
69{33{
70 connect('field.owner', 'onkeyup', update_branch_unique_name);34 connect('field.owner', 'onkeyup', update_branch_unique_name);
71 connect('field.owner', 'onchange', update_branch_unique_name);35 connect('field.owner', 'onchange', update_branch_unique_name);
72 connect('field.name', 'onkeyup', update_branch_unique_name);36 connect('field.name', 'onkeyup', update_branch_unique_name);
73 connect('field.branch_type.0', 'onclick', update_branch_url);
74 connect('field.branch_type.1', 'onclick', update_branch_url);
75 connect('field.branch_type.2', 'onclick', update_branch_url);
76 update_branch_unique_name();37 update_branch_unique_name();
77 connect('field.url', 'onchange', populate_branch_name_from_url);
78 connect('field.url', 'onblur', populate_branch_name_from_url);
79 document.getElementById("branch-unique-name-div").style.display = "block";38 document.getElementById("branch-unique-name-div").style.display = "block";
80}39}
8140
82registerLaunchpadFunction(hookUpBranchFieldFunctions);41registerLaunchpadFunction(hookUpBranchFieldFunctions);
83registerLaunchpadFunction(update_branch_url);
8442
85//]]>43//]]>
86</script>44</script>
8745
=== modified file 'lib/lp/code/templates/branch-import-details.pt'
--- lib/lp/code/templates/branch-import-details.pt 2010-06-10 07:54:59 +0000
+++ lib/lp/code/templates/branch-import-details.pt 2011-08-25 10:47:30 +0000
@@ -51,6 +51,12 @@
51 </p>51 </p>
52 </tal:hg-import>52 </tal:hg-import>
5353
54 <tal:bzr-import condition="code_import/rcs_type/enumvalue:BZR">
55 <p>This branch is an import of the Bazaar branch at
56 <span tal:replace="code_import/url" />.
57 </p>
58 </tal:bzr-import>
59
54 <tal:svn-import condition="view/is_svn_import">60 <tal:svn-import condition="view/is_svn_import">
55 <p id="svn-import-details">61 <p id="svn-import-details">
56 This branch is an import of the62 This branch is an import of the
5763
=== modified file 'lib/lp/code/templates/codeimport-new.pt'
--- lib/lp/code/templates/codeimport-new.pt 2010-04-22 23:50:51 +0000
+++ lib/lp/code/templates/codeimport-new.pt 2011-08-25 10:47:30 +0000
@@ -53,6 +53,20 @@
53 <tr>53 <tr>
54 <td>54 <td>
55 <label>55 <label>
56 <input tal:replace="structure view/rcs_type_bzr" />
57 Bazaar
58 </label>
59 <table class="importdetails">
60 <tal:widget define="widget nocall:view/widgets/bzr_branch_url">
61 <metal:block use-macro="context/@@launchpad_form/widget_row" />
62 </tal:widget>
63 </table>
64 </td>
65 </tr>
66
67 <tr>
68 <td>
69 <label>
56 <input tal:replace="structure view/rcs_type_git" />70 <input tal:replace="structure view/rcs_type_git" />
57 Git71 Git
58 </label>72 </label>
5973
=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py 2011-08-25 10:47:05 +0000
+++ lib/lp/registry/browser/productseries.py 2011-08-25 10:47:30 +0000
@@ -1090,8 +1090,7 @@
10901090
1091 # Create a new branch.1091 # Create a new branch.
1092 if branch_type == CREATE_NEW:1092 if branch_type == CREATE_NEW:
1093 branch = self._createBzrBranch(1093 branch = self._createBzrBranch(branch_name, branch_owner)
1094 BranchType.HOSTED, branch_name, branch_owner)
1095 if branch is not None:1094 if branch is not None:
1096 self.context.branch = branch1095 self.context.branch = branch
1097 self.request.response.addInfoNotification(1096 self.request.response.addInfoNotification(
@@ -1102,55 +1101,42 @@
1102 # Either create an externally hosted bzr branch1101 # Either create an externally hosted bzr branch
1103 # (a.k.a. 'mirrored') or create a new code import.1102 # (a.k.a. 'mirrored') or create a new code import.
1104 rcs_type = data.get('rcs_type')1103 rcs_type = data.get('rcs_type')
1105 if rcs_type == RevisionControlSystems.BZR:1104 # We need to create an import request.
1106 branch = self._createBzrBranch(1105 if rcs_type == RevisionControlSystems.CVS:
1107 BranchType.MIRRORED, branch_name, branch_owner,1106 cvs_root = data.get('repo_url')
1108 data['repo_url'])1107 cvs_module = data.get('cvs_module')
1109 if branch is None:1108 url = None
1110 return
1111
1112 self.context.branch = branch
1113 self.request.response.addInfoNotification(
1114 'Mirrored branch created and linked to '
1115 'the series.')
1116 else:1109 else:
1117 # We need to create an import request.1110 cvs_root = None
1118 if rcs_type == RevisionControlSystems.CVS:1111 cvs_module = None
1119 cvs_root = data.get('repo_url')1112 url = data.get('repo_url')
1120 cvs_module = data.get('cvs_module')1113 rcs_item = RevisionControlSystems.items[rcs_type.name]
1121 url = None1114 try:
1122 else:1115 code_import = getUtility(ICodeImportSet).new(
1123 cvs_root = None1116 registrant=branch_owner,
1124 cvs_module = None1117 target=IBranchTarget(self.context.product),
1125 url = data.get('repo_url')1118 branch_name=branch_name,
1126 rcs_item = RevisionControlSystems.items[rcs_type.name]1119 rcs_type=rcs_item,
1127 try:1120 url=url,
1128 code_import = getUtility(ICodeImportSet).new(1121 cvs_root=cvs_root,
1129 registrant=branch_owner,1122 cvs_module=cvs_module)
1130 target=IBranchTarget(self.context.product),1123 except BranchExists, e:
1131 branch_name=branch_name,1124 self._setBranchExists(e.existing_branch,
1132 rcs_type=rcs_item,1125 'branch_name')
1133 url=url,1126 self.errors_in_action = True
1134 cvs_root=cvs_root,1127 # Abort transaction. This is normally handled
1135 cvs_module=cvs_module)1128 # by LaunchpadFormView, but we are already in
1136 except BranchExists, e:1129 # the success handler.
1137 self._setBranchExists(e.existing_branch,1130 self._abort()
1138 'branch_name')1131 return
1139 self.errors_in_action = True1132 self.context.branch = code_import.branch
1140 # Abort transaction. This is normally handled1133 self.request.response.addInfoNotification(
1141 # by LaunchpadFormView, but we are already in1134 'Code import created and branch linked to the '
1142 # the success handler.1135 'series.')
1143 self._abort()
1144 return
1145 self.context.branch = code_import.branch
1146 self.request.response.addInfoNotification(
1147 'Code import created and branch linked to the '
1148 'series.')
1149 else:1136 else:
1150 raise UnexpectedFormData(branch_type)1137 raise UnexpectedFormData(branch_type)
11511138
1152 def _createBzrBranch(self, branch_type, branch_name,1139 def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
1153 branch_owner, repo_url=None):
1154 """Create a new Bazaar branch. It may be hosted or mirrored.1140 """Create a new Bazaar branch. It may be hosted or mirrored.
11551141
1156 Return the branch on success or None.1142 Return the branch on success or None.
@@ -1158,12 +1144,10 @@
1158 branch = None1144 branch = None
1159 try:1145 try:
1160 namespace = self.target.getNamespace(branch_owner)1146 namespace = self.target.getNamespace(branch_owner)
1161 branch = namespace.createBranch(branch_type=branch_type,1147 branch = namespace.createBranch(branch_type=BranchType.HOSTED,
1162 name=branch_name,1148 name=branch_name,
1163 registrant=self.user,1149 registrant=self.user,
1164 url=repo_url)1150 url=repo_url)
1165 if branch_type == BranchType.MIRRORED:
1166 branch.requestMirror()
1167 except BranchCreationForbidden:1151 except BranchCreationForbidden:
1168 self.addError(1152 self.addError(
1169 "You are not allowed to create branches in %s." %1153 "You are not allowed to create branches in %s." %
11701154
=== modified file 'lib/lp/registry/templates/productseries-codesummary.pt'
--- lib/lp/registry/templates/productseries-codesummary.pt 2011-05-16 20:55:08 +0000
+++ lib/lp/registry/templates/productseries-codesummary.pt 2011-08-25 10:47:30 +0000
@@ -54,7 +54,7 @@
54 </li>54 </li>
5555
56 <li>56 <li>
57 If the code is in Git, Mercurial, CVS or Subversion you can57 If the code is in Git, Mercurial, CVS, Subversion or an external Bazaar branch you can
58 <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>.58 <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>.
59 </li>59 </li>
60 </ul>60 </ul>

Subscribers

People subscribed via source and target branches

to status/vote changes: