Merge ~pelpsi/launchpad:differentiate-git-push-message-if-mp-exists into launchpad:master

Proposed by Simone Pelosi
Status: Merged
Approved by: Simone Pelosi
Approved revision: b26f294394437ac711a8ce4792244cbb2b4c8904
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pelpsi/launchpad:differentiate-git-push-message-if-mp-exists
Merge into: launchpad:master
Diff against target: 351 lines (+195/-18)
7 files modified
lib/lp/code/interfaces/branchmergeproposal.py (+7/-0)
lib/lp/code/interfaces/gitapi.py (+16/-0)
lib/lp/code/interfaces/gitrepository.py (+15/-0)
lib/lp/code/model/branchmergeproposal.py (+21/-17)
lib/lp/code/model/gitrepository.py (+8/-1)
lib/lp/code/xmlrpc/git.py (+71/-0)
lib/lp/code/xmlrpc/tests/test_git.py (+57/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+448597@code.launchpad.net

Commit message

Differentiate git push message if the MP exists

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
Simone Pelosi (pelpsi) wrote :

Squashed into one commit

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/interfaces/branchmergeproposal.py b/lib/lp/code/interfaces/branchmergeproposal.py
2index e65eef6..1b3c916 100644
3--- a/lib/lp/code/interfaces/branchmergeproposal.py
4+++ b/lib/lp/code/interfaces/branchmergeproposal.py
5@@ -971,6 +971,13 @@ class IBranchMergeProposalGetter(Interface):
6 def get(id):
7 """Return the BranchMergeProposal with specified id."""
8
9+ def activeProposalsForBranches(source, target=None):
10+ """Return BranchMergeProposals having a given source and target.
11+
12+ :param source: source branch
13+ :param target: target branch
14+ """
15+
16 def getProposalsForContext(context, status=None, visible_by_user=None):
17 """Return BranchMergeProposals associated with the context.
18
19diff --git a/lib/lp/code/interfaces/gitapi.py b/lib/lp/code/interfaces/gitapi.py
20index 5495944..98a241a 100644
21--- a/lib/lp/code/interfaces/gitapi.py
22+++ b/lib/lp/code/interfaces/gitapi.py
23@@ -95,6 +95,22 @@ class IGitAPI(Interface):
24 or an `Unauthorized` fault for unauthorized push attempts.
25 """
26
27+ def getMergeProposalInfo(translated_path, branch, auth_params):
28+ """Return the info (string) for a merge proposal.
29+
30+ When a `branch` that is not the default branch in a repository
31+ is pushed, the URL where the merge proposal for that branch can
32+ be opened will be generated and returned if the merge proposal
33+ doesn't exist, otherwise the link of the existing merge proposal
34+ will be returned.
35+
36+ :returns: A string explaining how to register a merge proposal
37+ for this branch, or pointing to an existing active merge
38+ proposal. A `GitRepositoryNotFound` fault is returned
39+ if no repository can be found for 'translated_path',
40+ or an `Unauthorized` fault for unauthorized push attempts.
41+ """
42+
43 def confirmRepoCreation(repository_id):
44 """Confirm that repository creation.
45
46diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
47index 7f6053f..4f57972 100644
48--- a/lib/lp/code/interfaces/gitrepository.py
49+++ b/lib/lp/code/interfaces/gitrepository.py
50@@ -787,6 +787,21 @@ class IGitRepositoryView(IHasRecipes):
51 diffs updated.
52 """
53
54+ def makeFrozenRef(path, commit_sha1):
55+ """A frozen Git reference.
56+
57+ This is like a GitRef, but is frozen at a particular commit, even if
58+ the real reference has moved on or has been deleted.
59+ It isn't necessarily backed by a real database object,
60+ and will retrieve columns from the database when required.
61+ Use this when you have a repository/path/commit_sha1 that you want
62+ to pass around as a single object,
63+ but don't necessarily know that the ref still exists.
64+
65+ :param path: the repository reference path.
66+ :param commit_sha1: the commit sha1 for that repository reference path.
67+ """
68+
69 def markRecipesStale(paths):
70 """Mark recipes associated with this repository as stale.
71
72diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
73index 924658c..73cae9a 100644
74--- a/lib/lp/code/model/branchmergeproposal.py
75+++ b/lib/lp/code/model/branchmergeproposal.py
76@@ -220,12 +220,9 @@ class BranchMergeProposal(StormBase, BugLinkTargetMixin):
77
78 @property
79 def source_git_ref(self):
80- from lp.code.model.gitref import GitRefFrozen
81-
82 if self.source_git_repository is None:
83 return None
84- return GitRefFrozen(
85- self.source_git_repository,
86+ return self.source_git_repository.makeFrozenRef(
87 self.source_git_path,
88 self.source_git_commit_sha1,
89 )
90@@ -238,12 +235,9 @@ class BranchMergeProposal(StormBase, BugLinkTargetMixin):
91
92 @property
93 def target_git_ref(self):
94- from lp.code.model.gitref import GitRefFrozen
95-
96 if self.target_git_repository is None:
97 return None
98- return GitRefFrozen(
99- self.target_git_repository,
100+ return self.target_git_repository.makeFrozenRef(
101 self.target_git_path,
102 self.target_git_commit_sha1,
103 )
104@@ -256,12 +250,9 @@ class BranchMergeProposal(StormBase, BugLinkTargetMixin):
105
106 @property
107 def prerequisite_git_ref(self):
108- from lp.code.model.gitref import GitRefFrozen
109-
110 if self.prerequisite_git_repository is None:
111 return None
112- return GitRefFrozen(
113- self.prerequisite_git_repository,
114+ return self.prerequisite_git_repository.makeFrozenRef(
115 self.prerequisite_git_path,
116 self.prerequisite_git_commit_sha1,
117 )
118@@ -1805,7 +1796,7 @@ class BranchMergeProposalGetter:
119 return result
120
121 @staticmethod
122- def activeProposalsForBranches(source, target):
123+ def activeProposalsForBranches(source, target=None):
124 clauses = [Not(BranchMergeProposal.queue_status.is_in(FINAL_STATES))]
125 if IGitRef.providedBy(source):
126 clauses.extend(
127@@ -1813,16 +1804,29 @@ class BranchMergeProposalGetter:
128 BranchMergeProposal.source_git_repository
129 == source.repository,
130 BranchMergeProposal.source_git_path == source.path,
131- BranchMergeProposal.target_git_repository
132- == target.repository,
133- BranchMergeProposal.target_git_path == target.path,
134 ]
135 )
136+
137+ if target is not None:
138+ clauses.extend(
139+ [
140+ BranchMergeProposal.target_git_repository
141+ == target.repository,
142+ BranchMergeProposal.target_git_path == target.path,
143+ ]
144+ )
145+
146 else:
147 clauses.extend(
148 [
149 BranchMergeProposal.source_branch == source,
150- BranchMergeProposal.target_branch == target,
151 ]
152 )
153+
154+ if target is not None:
155+ clauses.extend(
156+ [
157+ BranchMergeProposal.target_branch == target,
158+ ]
159+ )
160 return IStore(BranchMergeProposal).find(BranchMergeProposal, *clauses)
161diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
162index 87abb5b..a91df2e 100644
163--- a/lib/lp/code/model/gitrepository.py
164+++ b/lib/lp/code/model/gitrepository.py
165@@ -98,7 +98,7 @@ from lp.code.interfaces.revisionstatus import (
166 from lp.code.mail.branch import send_git_repository_modified_notifications
167 from lp.code.model.branchmergeproposal import BranchMergeProposal
168 from lp.code.model.gitactivity import GitActivity
169-from lp.code.model.gitref import GitRef, GitRefDefault
170+from lp.code.model.gitref import GitRef, GitRefDefault, GitRefFrozen
171 from lp.code.model.gitrule import GitRule, GitRuleGrant
172 from lp.code.model.gitsubscription import GitSubscription
173 from lp.code.model.revisionstatus import RevisionStatusReport
174@@ -1478,6 +1478,13 @@ class GitRepository(
175 jobs.extend(merge_proposal.scheduleDiffUpdates())
176 return jobs
177
178+ def makeFrozenRef(self, path, commit_sha1):
179+ return GitRefFrozen(
180+ self,
181+ path,
182+ commit_sha1,
183+ )
184+
185 def _getRecipes(self, paths=None):
186 """Undecorated version of recipes for use by `markRecipesStale`."""
187 from lp.code.model.sourcepackagerecipedata import (
188diff --git a/lib/lp/code/xmlrpc/git.py b/lib/lp/code/xmlrpc/git.py
189index 3b97a85..6e39e8c 100644
190--- a/lib/lp/code/xmlrpc/git.py
191+++ b/lib/lp/code/xmlrpc/git.py
192@@ -38,6 +38,7 @@ from lp.code.errors import (
193 GitRepositoryExists,
194 InvalidNamespace,
195 )
196+from lp.code.interfaces.branchmergeproposal import IBranchMergeProposalGetter
197 from lp.code.interfaces.codehosting import (
198 LAUNCHPAD_ANONYMOUS,
199 LAUNCHPAD_SERVICES,
200@@ -699,6 +700,76 @@ class GitAPI(LaunchpadXMLRPCView):
201 del result
202
203 @return_fault
204+ def _getMergeProposalInfo(
205+ self, requester, translated_path, ref, auth_params
206+ ):
207+ if requester == LAUNCHPAD_ANONYMOUS:
208+ requester = None
209+ repository = removeSecurityProxy(
210+ getUtility(IGitLookup).getByHostingPath(translated_path)
211+ )
212+ if repository is None:
213+ raise faults.GitRepositoryNotFound(translated_path)
214+
215+ verified = self._verifyAuthParams(requester, repository, auth_params)
216+ if verified is not None and verified.user is NO_USER:
217+ # Showing merge proposal information may be useful to ordinary
218+ # users, but it doesn't make sense in the context of
219+ # an internal service.
220+ return None
221+
222+ # commit_sha1 isn't used here, but is
223+ # needed to satisfy the `IGitRef` interface.
224+ frozen_ref = repository.makeFrozenRef(path=ref, commit_sha1="")
225+ branch = frozen_ref.name
226+ merge_proposals = getUtility(
227+ IBranchMergeProposalGetter
228+ ).activeProposalsForBranches(source=frozen_ref, target=None)
229+
230+ merge_proposal = merge_proposals.any()
231+ if merge_proposal:
232+ return (
233+ "Updated existing merge proposal "
234+ "for %s on Launchpad:\n %s"
235+ % (quote(branch), merge_proposal.address)
236+ )
237+ else:
238+ base_url = canonical_url(repository, rootsite="code")
239+ mp_url = "%s/+ref/%s/+register-merge" % (base_url, quote(branch))
240+ return (
241+ "Create a merge proposal for '%s' on "
242+ "Launchpad by visiting:\n %s" % (branch, mp_url)
243+ )
244+
245+ def getMergeProposalInfo(self, translated_path, ref, auth_params):
246+ """See `IGitAPI`."""
247+ logger = self._getLogger(auth_params.get("request-id"))
248+ requester_id = _get_requester_id(auth_params)
249+ logger.info(
250+ "Request received: getMergeProposalInfo('%s, %s') for %s",
251+ translated_path,
252+ ref,
253+ requester_id,
254+ )
255+
256+ result = run_with_login(
257+ requester_id,
258+ self._getMergeProposalInfo,
259+ translated_path,
260+ ref,
261+ auth_params,
262+ )
263+ try:
264+ if isinstance(result, xmlrpc.client.Fault):
265+ logger.error("getMergeProposalInfo failed: %r", result)
266+ else:
267+ logger.info("getMergeProposalInfo succeeded: %s" % result)
268+ return result
269+ finally:
270+ # Avoid traceback reference cycles.
271+ del result
272+
273+ @return_fault
274 def _authenticateWithPassword(self, username, password):
275 """Authenticate a user by username and password.
276
277diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
278index 1f69b8e..4764319 100644
279--- a/lib/lp/code/xmlrpc/tests/test_git.py
280+++ b/lib/lp/code/xmlrpc/tests/test_git.py
281@@ -660,6 +660,34 @@ class TestGitAPIMixin:
282 )
283 self.assertEqual(expected_mp_url, result)
284
285+ def assertHasMergeProposalInfo(
286+ self, repository, pushed_branch, auth_params, mp=None
287+ ):
288+ base_url = canonical_url(repository, rootsite="code")
289+ expected_mp_url = (
290+ "Create a merge proposal for 'branch1' on "
291+ "Launchpad by visiting:\n "
292+ "%s/+ref/%s/+register-merge"
293+ % (
294+ base_url,
295+ quote(pushed_branch),
296+ )
297+ )
298+
299+ if mp:
300+ expected_mp_url = (
301+ "Updated existing merge proposal "
302+ "for %s on Launchpad:\n %s"
303+ % (quote(pushed_branch), mp.address)
304+ )
305+
306+ result = self.git_api.getMergeProposalInfo(
307+ repository.getInternalPath(),
308+ "refs/heads/%s" % pushed_branch,
309+ auth_params,
310+ )
311+ self.assertEqual(expected_mp_url, result)
312+
313 def test_translatePath_private_repository(self):
314 requester = self.factory.makePerson()
315 repository = removeSecurityProxy(
316@@ -2760,6 +2788,35 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
317 access_token_id="string",
318 )
319
320+ # TODO: add more `getMergeProposalInfo` tests
321+ def test_getMergeProposalInfo(self):
322+ # Merge proposal information is returned by LP for a non-default branch
323+ # pushed by a user that has their ordinary privileges on the
324+ # corresponding repository.
325+ requester_owner = self.factory.makePerson()
326+ repository = self.factory.makeGitRepository(owner=requester_owner)
327+ [ref] = self.factory.makeGitRefs(
328+ repository=repository,
329+ paths=["refs/heads/master"],
330+ )
331+ [source_ref] = self.factory.makeGitRefs(
332+ repository=repository,
333+ paths=["refs/heads/branch1"],
334+ )
335+ removeSecurityProxy(repository).default_branch = "refs/heads/master"
336+ pushed_branch = "branch1"
337+ self.assertHasMergeProposalInfo(
338+ repository, pushed_branch, {"uid": requester_owner.id}
339+ )
340+
341+ mp = self.factory.makeBranchMergeProposalForGit(
342+ source_ref=source_ref, target_ref=ref
343+ )
344+
345+ self.assertHasMergeProposalInfo(
346+ repository, pushed_branch, {"uid": requester_owner.id}, mp
347+ )
348+
349 def test_getMergeProposalURL_user(self):
350 # A merge proposal URL is returned by LP for a non-default branch
351 # pushed by a user that has their ordinary privileges on the

Subscribers

People subscribed via source and target branches

to status/vote changes: