Merge ~ilasc/launchpad:git-push-url-hint into launchpad:master
- Git
- lp:~ilasc/launchpad
- git-push-url-hint
- Merge into master
Status: | Merged |
---|---|
Approved by: | Ioana Lasc |
Approved revision: | 6909a8bdba9f2113c45199545d7e628b34cd5dde |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ilasc/launchpad:git-push-url-hint |
Merge into: | launchpad:master |
Diff against target: |
555 lines (+321/-28) 9 files modified
lib/lp/code/browser/gitref.py (+22/-1) lib/lp/code/browser/gitrepository.py (+20/-1) lib/lp/code/browser/tests/test_gitlisting.py (+1/-1) lib/lp/code/browser/tests/test_gitref.py (+115/-0) lib/lp/code/browser/tests/test_gitrepository.py (+118/-16) lib/lp/code/interfaces/gitnamespace.py (+3/-0) lib/lp/code/interfaces/gitrepository.py (+20/-1) lib/lp/code/model/gitnamespace.py (+4/-0) lib/lp/code/templates/git-macros.pt (+18/-8) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+386567@code.launchpad.net |
Commit message
Add git push URL hinting
Description of the change
Add git push URL hinting to LP UI on both repository and branch pages when the user doesn’t have permission to push to the repository.
Colin Watson (cjwatson) : | # |
- 17b83c2... by Ioana Lasc
-
Subclass GitIdentityMixin for contributor repository path
Colin Watson (cjwatson) wrote : | # |
Thanks, this is an improvement. I'd like to see some more polishing of the contributor-
- a83386a... by Ioana Lasc
-
Address code review comments
Ioana Lasc (ilasc) wrote : | # |
Thank you Colin!
I've addressed everything and in relation to your comment on the 2 code paths in git_ssh_
Module lp.code.
for default in self.getReposit
Module lp.code.
"Only projects, packages, or OCI projects can have "
The branch is now ready for another look.
Colin Watson (cjwatson) wrote : | # |
git_ssh_
https:/
There's this new import policy warning from the test suite, which should be resolved one way or another:
There were 2 imports of names not appearing in the __all__.
You should not import ContributorGitI
lp.
lp.
There's also one further case that may have been overlooked here. Personal repositories (those handled by lp.code.
Despite my repeated "Needs fixing" votes, this is definitely improving, and I think is nearly there! Hopefully it should just be this one more round.
- 281c8a9... by Ioana Lasc
-
Address code review comments
Ioana Lasc (ilasc) wrote : | # |
Colin thank you for the helpful comments.
MP is now ready for another review.
Colin Watson (cjwatson) wrote : | # |
I'd like a few more bits of tidying up, explained in detail below, but this looks pretty close now. Thanks for your work.
- a9e7a78... by Ioana Lasc
-
Merge branch 'master' into git-push-url-hint
- 5dd6be6... by Ioana Lasc
-
Address code review comments
Colin Watson (cjwatson) : | # |
- 6909a8b... by Ioana Lasc
-
Fix unit tests
Ioana Lasc (ilasc) wrote : | # |
Thanks Colin! Agreed with all comments & suggestions.
Had to tweak a few tests I didn't write for the new code, possibly worth another look before I land this ?
Colin Watson (cjwatson) : | # |
Preview Diff
1 | diff --git a/lib/lp/code/browser/gitref.py b/lib/lp/code/browser/gitref.py |
2 | index a945fa0..f17207e 100644 |
3 | --- a/lib/lp/code/browser/gitref.py |
4 | +++ b/lib/lp/code/browser/gitref.py |
5 | @@ -13,6 +13,7 @@ __all__ = [ |
6 | |
7 | import json |
8 | |
9 | +from breezy import urlutils |
10 | from lazr.restful.interface import copy_field |
11 | from six.moves.urllib_parse import ( |
12 | quote_plus, |
13 | @@ -46,8 +47,12 @@ from lp.code.errors import InvalidBranchMergeProposal |
14 | from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal |
15 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
16 | from lp.code.interfaces.gitref import IGitRef |
17 | -from lp.code.interfaces.gitrepository import IGitRepositorySet |
18 | +from lp.code.interfaces.gitrepository import ( |
19 | + ContributorGitIdentity, |
20 | + IGitRepositorySet, |
21 | + ) |
22 | from lp.registry.interfaces.person import IPerson |
23 | +from lp.services.config import config |
24 | from lp.services.helpers import english_list |
25 | from lp.services.propertycache import cachedproperty |
26 | from lp.services.scripts import log |
27 | @@ -120,6 +125,22 @@ class GitRefView(LaunchpadView, HasSnapsViewMixin): |
28 | return urlunsplit(url) |
29 | |
30 | @property |
31 | + def git_ssh_url_non_owner(self): |
32 | + """The git+ssh:// URL for this repository, adjusted for this user. |
33 | + |
34 | + The user is not the owner of the repository. |
35 | + """ |
36 | + contributor = ContributorGitIdentity( |
37 | + owner=self.user, |
38 | + target=self.context.repository.target, |
39 | + repository=self.context.repository) |
40 | + base_url = urlutils.join( |
41 | + config.codehosting.git_ssh_root, contributor.shortened_path) |
42 | + url = list(urlsplit(base_url)) |
43 | + url[1] = "{}@{}".format(self.user.name, url[1]) |
44 | + return urlunsplit(url) |
45 | + |
46 | + @property |
47 | def user_can_push(self): |
48 | """Whether the user can push to this branch.""" |
49 | return ( |
50 | diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py |
51 | index 7dc1c32..76b9f7d 100644 |
52 | --- a/lib/lp/code/browser/gitrepository.py |
53 | +++ b/lib/lp/code/browser/gitrepository.py |
54 | @@ -25,6 +25,7 @@ __all__ = [ |
55 | import base64 |
56 | from collections import defaultdict |
57 | |
58 | +from breezy import urlutils |
59 | from lazr.lifecycle.event import ObjectModifiedEvent |
60 | from lazr.lifecycle.snapshot import Snapshot |
61 | from lazr.restful.interface import ( |
62 | @@ -99,7 +100,10 @@ from lp.code.errors import ( |
63 | ) |
64 | from lp.code.interfaces.gitnamespace import get_git_namespace |
65 | from lp.code.interfaces.gitref import IGitRefBatchNavigator |
66 | -from lp.code.interfaces.gitrepository import IGitRepository |
67 | +from lp.code.interfaces.gitrepository import ( |
68 | + ContributorGitIdentity, |
69 | + IGitRepository, |
70 | + ) |
71 | from lp.code.vocabularies.gitrule import GitPermissionsVocabulary |
72 | from lp.registry.interfaces.person import ( |
73 | IPerson, |
74 | @@ -385,6 +389,21 @@ class GitRepositoryView(InformationTypePortletMixin, LaunchpadView, |
75 | return urlunsplit(url) |
76 | |
77 | @property |
78 | + def git_ssh_url_non_owner(self): |
79 | + """The git+ssh:// URL for this repository, adjusted for this user. |
80 | + The user is not the owner of the repository.""" |
81 | + |
82 | + contributor = ContributorGitIdentity( |
83 | + owner=self.user, |
84 | + target=self.context.target, |
85 | + repository=self.context) |
86 | + base_url = urlutils.join( |
87 | + config.codehosting.git_ssh_root, contributor.shortened_path) |
88 | + url = list(urlsplit(base_url)) |
89 | + url[1] = "{}@{}".format(self.user.name, url[1]) |
90 | + return urlunsplit(url) |
91 | + |
92 | + @property |
93 | def user_can_push(self): |
94 | """Whether the user can push to this branch.""" |
95 | return ( |
96 | diff --git a/lib/lp/code/browser/tests/test_gitlisting.py b/lib/lp/code/browser/tests/test_gitlisting.py |
97 | index 72954ad..e7682fc 100644 |
98 | --- a/lib/lp/code/browser/tests/test_gitlisting.py |
99 | +++ b/lib/lp/code/browser/tests/test_gitlisting.py |
100 | @@ -125,7 +125,7 @@ class TestTargetGitListingView: |
101 | repository=other_repo, user=other_repo.owner) |
102 | |
103 | self.assertThat( |
104 | - self.target, BrowsesWithQueryLimit(36, self.owner, '+git')) |
105 | + self.target, BrowsesWithQueryLimit(38, self.owner, '+git')) |
106 | |
107 | def test_copes_with_no_default(self): |
108 | self.factory.makeGitRepository( |
109 | diff --git a/lib/lp/code/browser/tests/test_gitref.py b/lib/lp/code/browser/tests/test_gitref.py |
110 | index 0268ca4..0a36550 100644 |
111 | --- a/lib/lp/code/browser/tests/test_gitref.py |
112 | +++ b/lib/lp/code/browser/tests/test_gitref.py |
113 | @@ -33,6 +33,8 @@ from lp.services.webapp.publisher import canonical_url |
114 | from lp.testing import ( |
115 | admin_logged_in, |
116 | BrowserTestCase, |
117 | + login_person, |
118 | + person_logged_in, |
119 | StormStatementRecorder, |
120 | TestCaseWithFactory, |
121 | ) |
122 | @@ -155,6 +157,119 @@ class TestGitRefView(BrowserTestCase): |
123 | git clone -b branch git\+ssh://{username}@git.launchpad.test/.* |
124 | """.format(username=username), text) |
125 | |
126 | + def test_push_directions_logged_in_cannot_push_individual(self): |
127 | + repo = self.factory.makeGitRepository() |
128 | + [ref] = self.factory.makeGitRefs(repository=repo, |
129 | + paths=["refs/heads/branch"]) |
130 | + login_person(self.user) |
131 | + view = create_initialized_view(ref, "+index", principal=self.user) |
132 | + git_push_url_text_match = soupmatchers.HTMLContains( |
133 | + soupmatchers.Tag( |
134 | + 'Push url text', 'dt', |
135 | + text='To fork this repository and propose ' |
136 | + 'fixes from there, push to this repository:')) |
137 | + |
138 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
139 | + soupmatchers.Tag( |
140 | + 'Push url hint', 'span', |
141 | + text=('git+ssh://%s@git.launchpad.test/' |
142 | + '~%s/%s') % ( |
143 | + self.user.name, self.user.name, repo.target.name))) |
144 | + with person_logged_in(self.user): |
145 | + rendered_view = view.render() |
146 | + self.assertThat(rendered_view, git_push_url_text_match) |
147 | + self.assertThat(rendered_view, git_push_url_hint_match) |
148 | + |
149 | + def test_push_directions_logged_in_cannot_push_individual_project(self): |
150 | + # Repository is the default for a project |
151 | + eric = self.factory.makePerson(name="eric") |
152 | + fooix = self.factory.makeProduct(name="fooix", owner=eric) |
153 | + repository = self.factory.makeGitRepository( |
154 | + owner=eric, target=fooix, name="fooix-repo") |
155 | + [ref] = self.factory.makeGitRefs(repository=repository, |
156 | + paths=["refs/heads/branch"]) |
157 | + self.repository_set = getUtility(IGitRepositorySet) |
158 | + with person_logged_in(fooix.owner) as user: |
159 | + self.repository_set.setDefaultRepositoryForOwner( |
160 | + repository.owner, fooix, repository, user) |
161 | + self.repository_set.setDefaultRepository(fooix, repository) |
162 | + login_person(self.user) |
163 | + view = create_initialized_view(ref, "+index", principal=self.user) |
164 | + git_push_url_text_match = soupmatchers.HTMLContains( |
165 | + soupmatchers.Tag( |
166 | + 'Push url text', 'dt', |
167 | + text='To fork this repository and propose ' |
168 | + 'fixes from there, push to this repository:')) |
169 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
170 | + soupmatchers.Tag( |
171 | + 'Push url hint', 'span', |
172 | + text='git+ssh://%s@git.launchpad.test/~%s/%s' % |
173 | + (self.user.name, |
174 | + self.user.name, |
175 | + repository.target.name))) |
176 | + |
177 | + with person_logged_in(self.user): |
178 | + rendered_view = view.render() |
179 | + self.assertThat(rendered_view, git_push_url_text_match) |
180 | + self.assertThat(rendered_view, git_push_url_hint_match) |
181 | + |
182 | + def test_push_directions_logged_in_cannot_push_individual_package(self): |
183 | + # Repository is the default for a package |
184 | + mint = self.factory.makeDistribution(name="mint") |
185 | + eric = self.factory.makePerson(name="eric") |
186 | + mint_choc = self.factory.makeDistributionSourcePackage( |
187 | + distribution=mint, sourcepackagename="choc") |
188 | + repository = self.factory.makeGitRepository( |
189 | + owner=eric, target=mint_choc, name="choc-repo") |
190 | + [ref] = self.factory.makeGitRefs(repository=repository, |
191 | + paths=["refs/heads/branch"]) |
192 | + dsp = repository.target |
193 | + self.repository_set = getUtility(IGitRepositorySet) |
194 | + with admin_logged_in(): |
195 | + self.repository_set.setDefaultRepositoryForOwner( |
196 | + repository.owner, dsp, repository, repository.owner) |
197 | + self.repository_set.setDefaultRepository(dsp, repository) |
198 | + login_person(self.user) |
199 | + view = create_initialized_view(ref, "+index", principal=self.user) |
200 | + git_push_url_text_match = soupmatchers.HTMLContains( |
201 | + soupmatchers.Tag( |
202 | + 'Push url text', 'dt', |
203 | + text='To fork this repository and propose ' |
204 | + 'fixes from there, push to this repository:')) |
205 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
206 | + soupmatchers.Tag( |
207 | + 'Push url hint', 'span', |
208 | + text='git+ssh://%s@git.launchpad.test/~%s/%s/+source/%s' % |
209 | + (self.user.name, |
210 | + self.user.name, |
211 | + mint.name, |
212 | + mint_choc.name))) |
213 | + |
214 | + with person_logged_in(self.user): |
215 | + rendered_view = view.render() |
216 | + self.assertThat(rendered_view, git_push_url_text_match) |
217 | + self.assertThat(rendered_view, git_push_url_hint_match) |
218 | + |
219 | + def test_push_directions_logged_in_cannot_push_personal_project(self): |
220 | + repository = self.factory.makeGitRepository( |
221 | + owner=self.user, target=self.user) |
222 | + [ref] = self.factory.makeGitRefs(repository=repository, |
223 | + paths=["refs/heads/branch"]) |
224 | + other_user = self.factory.makePerson() |
225 | + login_person(other_user) |
226 | + view = create_initialized_view(ref, "+index", principal=self.user) |
227 | + git_push_url_text_match = soupmatchers.Tag( |
228 | + 'Push url text', 'a', |
229 | + text=self.user.displayname) |
230 | + with person_logged_in(other_user): |
231 | + rendered_view = view.render() |
232 | + div = soupmatchers.Tag("Push directions", "div", |
233 | + attrs={"id": "push-directions"}) |
234 | + self.assertThat(rendered_view, soupmatchers.HTMLContains( |
235 | + soupmatchers.Within( |
236 | + div, |
237 | + git_push_url_text_match))) |
238 | + |
239 | def makeCommitLog(self): |
240 | authors = [self.factory.makePerson() for _ in range(5)] |
241 | with admin_logged_in(): |
242 | diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py |
243 | index d323976..d3211ba 100644 |
244 | --- a/lib/lp/code/browser/tests/test_gitrepository.py |
245 | +++ b/lib/lp/code/browser/tests/test_gitrepository.py |
246 | @@ -50,6 +50,7 @@ from lp.code.enums import ( |
247 | GitRepositoryStatus, |
248 | GitRepositoryType, |
249 | ) |
250 | +from lp.code.interfaces.gitrepository import IGitRepositorySet |
251 | from lp.code.interfaces.revision import IRevisionSet |
252 | from lp.code.model.gitjob import GitRefScanJob |
253 | from lp.code.tests.helpers import GitHostingFixture |
254 | @@ -225,6 +226,7 @@ class TestGitRepositoryView(BrowserTestCase): |
255 | Update this repository: |
256 | git push |
257 | git+ssh://{username}@git.launchpad.test/{repository.shortened_path} |
258 | + BRANCHNAME |
259 | """).format(username=username, repository=repository), |
260 | flags=doctest.NORMALIZE_WHITESPACE)) |
261 | |
262 | @@ -246,30 +248,130 @@ class TestGitRepositoryView(BrowserTestCase): |
263 | # If the user is logged in but cannot push to a repository owned by |
264 | # a person, we explain who can push. |
265 | repository = self.factory.makeGitRepository() |
266 | - browser = self.getViewBrowser(repository) |
267 | - directions = find_tag_by_id(browser.contents, "push-directions") |
268 | login_person(self.user) |
269 | - self.assertThat(directions.renderContents(), DocTestMatches(dedent(""" |
270 | - You cannot push to this repository. Only <a |
271 | - href="http://launchpad.test/~{owner.name}">{owner.display_name}</a> |
272 | - can push to this repository. |
273 | - """).format(owner=repository.owner), |
274 | - flags=doctest.NORMALIZE_WHITESPACE)) |
275 | + view = create_initialized_view( |
276 | + repository, '+index', principal=self.user) |
277 | + git_push_url_text_match = soupmatchers.HTMLContains( |
278 | + soupmatchers.Tag( |
279 | + 'Push url text', 'dt', |
280 | + text='To fork this repository and propose ' |
281 | + 'fixes from there, push to this repository:')) |
282 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
283 | + soupmatchers.Tag( |
284 | + 'Push url hint', 'span', |
285 | + text='git+ssh://%s@git.launchpad.test/~%s/%s' % |
286 | + (self.user.name, self.user.name, repository.target.name))) |
287 | + with person_logged_in(self.user): |
288 | + rendered_view = view.render() |
289 | + self.assertThat(rendered_view, git_push_url_text_match) |
290 | + self.assertThat(rendered_view, git_push_url_hint_match) |
291 | + |
292 | + def test_push_directions_logged_in_cannot_push_individual_project(self): |
293 | + # Repository is the default for a project |
294 | + eric = self.factory.makePerson(name="eric") |
295 | + fooix = self.factory.makeProduct(name="fooix", owner=eric) |
296 | + repository = self.factory.makeGitRepository( |
297 | + owner=eric, target=fooix, name="fooix-repo") |
298 | + self.repository_set = getUtility(IGitRepositorySet) |
299 | + with person_logged_in(fooix.owner) as user: |
300 | + self.repository_set.setDefaultRepositoryForOwner( |
301 | + repository.owner, fooix, repository, user) |
302 | + self.repository_set.setDefaultRepository(fooix, repository) |
303 | + login_person(self.user) |
304 | + view = create_initialized_view( |
305 | + repository, '+index', principal=self.user) |
306 | + git_push_url_text_match = soupmatchers.HTMLContains( |
307 | + soupmatchers.Tag( |
308 | + 'Push url text', 'dt', |
309 | + text='To fork this repository and propose ' |
310 | + 'fixes from there, push to this repository:')) |
311 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
312 | + soupmatchers.Tag( |
313 | + 'Push url hint', 'span', |
314 | + text='git+ssh://%s@git.launchpad.test/~%s/%s' % |
315 | + (self.user.name, self.user.name, |
316 | + repository.target.name))) |
317 | + with person_logged_in(self.user): |
318 | + rendered_view = view.render() |
319 | + self.assertThat(rendered_view, git_push_url_text_match) |
320 | + self.assertThat(rendered_view, git_push_url_hint_match) |
321 | + |
322 | + def test_push_directions_logged_in_cannot_push_individual_package(self): |
323 | + # Repository is the default for a package |
324 | + mint = self.factory.makeDistribution(name="mint") |
325 | + eric = self.factory.makePerson(name="eric") |
326 | + mint_choc = self.factory.makeDistributionSourcePackage( |
327 | + distribution=mint, sourcepackagename="choc") |
328 | + repository = self.factory.makeGitRepository( |
329 | + owner=eric, target=mint_choc, name="choc-repo") |
330 | + dsp = repository.target |
331 | + self.repository_set = getUtility(IGitRepositorySet) |
332 | + with admin_logged_in(): |
333 | + self.repository_set.setDefaultRepositoryForOwner( |
334 | + repository.owner, dsp, repository, repository.owner) |
335 | + self.repository_set.setDefaultRepository(dsp, repository) |
336 | + login_person(self.user) |
337 | + view = create_initialized_view( |
338 | + repository, '+index', principal=self.user) |
339 | + git_push_url_text_match = soupmatchers.HTMLContains( |
340 | + soupmatchers.Tag( |
341 | + 'Push url text', 'dt', |
342 | + text='To fork this repository and propose ' |
343 | + 'fixes from there, push to this repository:')) |
344 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
345 | + soupmatchers.Tag( |
346 | + 'Push url hint', 'span', |
347 | + text='git+ssh://%s@git.launchpad.test/~%s/%s/+source/%s' % |
348 | + (self.user.name, self.user.name, |
349 | + mint.name, mint_choc.name))) |
350 | + with person_logged_in(self.user): |
351 | + rendered_view = view.render() |
352 | + self.assertThat(rendered_view, git_push_url_text_match) |
353 | + self.assertThat(rendered_view, git_push_url_hint_match) |
354 | + |
355 | + def test_push_directions_logged_in_cannot_push_personal_project(self): |
356 | + # If the user is logged in but cannot push to a repository owned by |
357 | + # a person, we explain who can push. |
358 | + repository = self.factory.makeGitRepository( |
359 | + owner=self.user, target=self.user) |
360 | + other_user = self.factory.makePerson() |
361 | + login_person(other_user) |
362 | + view = create_initialized_view( |
363 | + repository, '+index', principal=other_user) |
364 | + git_push_url_text_match = soupmatchers.Tag( |
365 | + 'Push url text', 'a', |
366 | + text=self.user.displayname) |
367 | + with person_logged_in(other_user): |
368 | + rendered_view = view.render() |
369 | + div = soupmatchers.Tag("Push directions", "div", |
370 | + attrs={"id": "push-directions"}) |
371 | + self.assertThat(rendered_view, soupmatchers.HTMLContains( |
372 | + soupmatchers.Within( |
373 | + div, |
374 | + git_push_url_text_match))) |
375 | |
376 | def test_push_directions_logged_in_cannot_push_team(self): |
377 | # If the user is logged in but cannot push to a repository owned by |
378 | # a team, we explain who can push. |
379 | team = self.factory.makeTeam() |
380 | repository = self.factory.makeGitRepository(owner=team) |
381 | - browser = self.getViewBrowser(repository) |
382 | - directions = find_tag_by_id(browser.contents, "push-directions") |
383 | login_person(self.user) |
384 | - self.assertThat(directions.renderContents(), DocTestMatches(dedent(""" |
385 | - You cannot push to this repository. Members of <a |
386 | - href="http://launchpad.test/~{owner.name}">{owner.display_name}</a> |
387 | - can push to this repository. |
388 | - """).format(owner=repository.owner), |
389 | - flags=doctest.NORMALIZE_WHITESPACE)) |
390 | + view = create_initialized_view( |
391 | + repository, '+index', principal=self.user) |
392 | + git_push_url_text_match = soupmatchers.HTMLContains( |
393 | + soupmatchers.Tag( |
394 | + 'Push url text', 'dt', |
395 | + text='To fork this repository and propose ' |
396 | + 'fixes from there, push to this repository:')) |
397 | + git_push_url_hint_match = soupmatchers.HTMLContains( |
398 | + soupmatchers.Tag( |
399 | + 'Push url hint', 'span', |
400 | + text='git+ssh://%s@git.launchpad.test/~%s/%s' % |
401 | + (self.user.name, self.user.name, repository.target.name))) |
402 | + with person_logged_in(self.user): |
403 | + rendered_view = view.render() |
404 | + self.assertThat(rendered_view, git_push_url_text_match) |
405 | + self.assertThat(rendered_view, git_push_url_hint_match) |
406 | |
407 | def test_no_push_directions_for_imported_repository(self): |
408 | # Imported repositories never show push directions. |
409 | diff --git a/lib/lp/code/interfaces/gitnamespace.py b/lib/lp/code/interfaces/gitnamespace.py |
410 | index fd7e3e7..9c6886b 100644 |
411 | --- a/lib/lp/code/interfaces/gitnamespace.py |
412 | +++ b/lib/lp/code/interfaces/gitnamespace.py |
413 | @@ -99,6 +99,9 @@ class IGitNamespacePolicy(Interface): |
414 | "True iff this namespace permits automatically setting a default " |
415 | "repository on push.") |
416 | |
417 | + show_push_url_hints = Attribute( |
418 | + "True if this namespace permits display of the push URL hint.") |
419 | + |
420 | supports_merge_proposals = Attribute( |
421 | "Does this namespace support merge proposals at all?") |
422 | |
423 | diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py |
424 | index 189b426..344bc43 100644 |
425 | --- a/lib/lp/code/interfaces/gitrepository.py |
426 | +++ b/lib/lp/code/interfaces/gitrepository.py |
427 | @@ -6,6 +6,7 @@ |
428 | __metaclass__ = type |
429 | |
430 | __all__ = [ |
431 | + 'ContributorGitIdentity', |
432 | 'GitIdentityMixin', |
433 | 'GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE', |
434 | 'git_repository_name_validator', |
435 | @@ -1033,7 +1034,8 @@ class IGitRepositorySet(Interface): |
436 | "checked."), |
437 | schema=IPerson), |
438 | repository_names=List(value_type=Text(), |
439 | - title=_('List of repository unique names'), required=True), |
440 | + title=_('List of repository unique names'), |
441 | + required=True), |
442 | ) |
443 | @export_read_operation() |
444 | @operation_for_version("devel") |
445 | @@ -1228,3 +1230,20 @@ def user_has_special_git_repository_access(user, repository=None): |
446 | if code_import is None: |
447 | return False |
448 | return roles.in_vcs_imports |
449 | + |
450 | + |
451 | +class ContributorGitIdentity(GitIdentityMixin): |
452 | + |
453 | + def __init__(self, owner, |
454 | + target, repository): |
455 | + self.target_default = False |
456 | + self.owner_default = True |
457 | + self.owner = owner |
458 | + self.target = target |
459 | + self.repository = repository |
460 | + |
461 | + def getRepositoryIdentities(self): |
462 | + identities = [ |
463 | + (default.path, default.context) |
464 | + for default in self.getRepositoryDefaults()] |
465 | + return identities |
466 | diff --git a/lib/lp/code/model/gitnamespace.py b/lib/lp/code/model/gitnamespace.py |
467 | index 8a08a97..f9ac2f5 100644 |
468 | --- a/lib/lp/code/model/gitnamespace.py |
469 | +++ b/lib/lp/code/model/gitnamespace.py |
470 | @@ -273,6 +273,7 @@ class PersonalGitNamespace(_BaseGitNamespace): |
471 | supports_merge_proposals = True |
472 | supports_code_imports = False |
473 | allow_recipe_name_from_target = False |
474 | + show_push_url_hints = False |
475 | |
476 | def __init__(self, person): |
477 | self.owner = person |
478 | @@ -356,6 +357,7 @@ class ProjectGitNamespace(_BaseGitNamespace): |
479 | supports_merge_proposals = True |
480 | supports_code_imports = True |
481 | allow_recipe_name_from_target = True |
482 | + show_push_url_hints = True |
483 | |
484 | def __init__(self, person, project): |
485 | self.owner = person |
486 | @@ -447,6 +449,7 @@ class PackageGitNamespace(_BaseGitNamespace): |
487 | supports_merge_proposals = True |
488 | supports_code_imports = True |
489 | allow_recipe_name_from_target = True |
490 | + show_push_url_hints = True |
491 | |
492 | def __init__(self, person, distro_source_package): |
493 | self.owner = person |
494 | @@ -538,6 +541,7 @@ class OCIProjectGitNamespace(_BaseGitNamespace): |
495 | supports_merge_proposals = True |
496 | supports_code_imports = True |
497 | allow_recipe_name_from_target = True |
498 | + show_push_url_hints = False |
499 | |
500 | def __init__(self, person, oci_project): |
501 | self.owner = person |
502 | diff --git a/lib/lp/code/templates/git-macros.pt b/lib/lp/code/templates/git-macros.pt |
503 | index 9407df1..928bf83 100644 |
504 | --- a/lib/lp/code/templates/git-macros.pt |
505 | +++ b/lib/lp/code/templates/git-macros.pt |
506 | @@ -64,31 +64,41 @@ |
507 | <tt class="command"> |
508 | git push |
509 | <span class="ssh-url" tal:content="view/git_ssh_url" /> |
510 | - <tal:branch condition="branch_name|nothing" replace="branch_name" /> |
511 | + <em>BRANCHNAME</em> |
512 | </tt> |
513 | </dd> |
514 | </dl> |
515 | - <p tal:condition="not:view/user/sshkeys" id="ssh-key-directions"> |
516 | - To authenticate with the Launchpad Git hosting service, you need to |
517 | - <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys"> |
518 | - register an SSH key</a>. |
519 | - </p> |
520 | </tal:can-push> |
521 | |
522 | <tal:cannot-push condition="not:view/user_can_push"> |
523 | <tal:individual condition="not:context/owner/is_team"> |
524 | - You cannot push to this <tal:kind replace="kind" />. Only |
525 | + You cannot push directly to this <tal:kind replace="kind" />. Only |
526 | <a tal:attributes="href context/owner/fmt:url" |
527 | tal:content="context/owner/displayname">Person</a> |
528 | can push to this <tal:kind replace="kind" />. |
529 | </tal:individual> |
530 | <tal:team condition="context/owner/is_team"> |
531 | - You cannot push to this <tal:kind replace="kind" />. Members of |
532 | + You cannot push directly to this <tal:kind replace="kind" />. Members of |
533 | <a tal:attributes="href context/owner/fmt:url" |
534 | tal:content="context/owner/displayname">Team</a> |
535 | can push to this <tal:kind replace="kind" />. |
536 | </tal:team> |
537 | + <dl tal:condition="context/namespace/show_push_url_hints" id="push-url"> |
538 | + <dt>To fork this repository and propose fixes from there, push to this repository:</dt> |
539 | + <dd> |
540 | + <tt class="command"> |
541 | + git push |
542 | + <span class="ssh-url" tal:content="view/git_ssh_url_non_owner" /> |
543 | + <em>BRANCHNAME</em> |
544 | + </tt> |
545 | + </dd> |
546 | + </dl> |
547 | </tal:cannot-push> |
548 | + <p tal:condition="not:view/user/sshkeys" id="ssh-key-directions"> |
549 | + To authenticate with the Launchpad Git hosting service, you need to |
550 | + <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys"> |
551 | + register an SSH key</a>. |
552 | + </p> |
553 | </tal:logged-in> |
554 | |
555 | </div> |
Looks like a good job. I just have a question about the splitting of the URL itself.