Merge lp:~wgrant/launchpad/webhook-git-push into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 17690
Proposed branch: lp:~wgrant/launchpad/webhook-git-push
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/webhook-trigger
Diff against target: 163 lines (+96/-0)
3 files modified
database/schema/security.cfg (+2/-0)
lib/lp/code/model/gitjob.py (+29/-0)
lib/lp/code/model/tests/test_gitjob.py (+65/-0)
To merge this branch: bzr merge lp:~wgrant/launchpad/webhook-git-push
Reviewer Review Type Date Requested Status
Kit Randel (community) Approve
Review via email: mp+267510@code.launchpad.net

Commit message

Trigger git:push:0.1 webhooks as part of GitRefScanJobs.

Description of the change

Trigger git:push:0.1 webhooks as part of GitRefScanJobs.

The payload is currently quite limited: the git repository's shortened path, and a dict containing old and new SHA-1s for each changed ref. Details of commits aren't readily accessible without some refactoring, and we'll see what people actually want.

To post a comment you must log in.
Revision history for this message
Kit Randel (blr) wrote :

One query around the payload (realise this is a first pass), otherwise looks great.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-08-07 11:25:04 +0000
3+++ database/schema/security.cfg 2015-08-10 12:52:50 +0000
4@@ -712,6 +712,8 @@
5 public.translationtemplatesbuild = SELECT, INSERT
6 public.validpersoncache = SELECT
7 public.validpersonorteamcache = SELECT
8+public.webhook = SELECT
9+public.webhookjob = SELECT, INSERT
10 type=user
11
12 [branch-distro]
13
14=== modified file 'lib/lp/code/model/gitjob.py'
15--- lib/lp/code/model/gitjob.py 2015-07-09 20:06:17 +0000
16+++ lib/lp/code/model/gitjob.py 2015-08-10 12:52:50 +0000
17@@ -51,6 +51,7 @@
18 try_advisory_lock,
19 )
20 from lp.services.database.stormbase import StormBase
21+from lp.services.features import getFeatureFlag
22 from lp.services.job.model.job import (
23 EnumeratedSubclass,
24 Job,
25@@ -58,6 +59,7 @@
26 from lp.services.job.runner import BaseRunnableJob
27 from lp.services.mail.sendmail import format_address_for_person
28 from lp.services.scripts import log
29+from lp.services.webhooks.interfaces import IWebhookSet
30
31
32 class GitJobType(DBEnumeratedType):
33@@ -196,6 +198,26 @@
34 job.celeryRunOnCommit()
35 return job
36
37+ @staticmethod
38+ def composeWebhookPayload(repository, refs_to_upsert, refs_to_remove):
39+ old_refs = {ref.path: ref for ref in repository.refs}
40+ ref_changes = {}
41+ for ref in refs_to_upsert.keys() + list(refs_to_remove):
42+ old = (
43+ {"commit_sha1": old_refs[ref].commit_sha1}
44+ if ref in old_refs else None)
45+ new = (
46+ {"commit_sha1": refs_to_upsert[ref]['sha1']}
47+ if ref in refs_to_upsert else None)
48+ # planRefChanges can return an unchanged ref if the cached
49+ # commit details differ.
50+ if old != new:
51+ ref_changes[ref] = {"old": old, "new": new}
52+ return {
53+ "git_repository_path": repository.shortened_path,
54+ "ref_changes": ref_changes,
55+ }
56+
57 def run(self):
58 """See `IGitRefScanJob`."""
59 try:
60@@ -207,6 +229,13 @@
61 self.repository.planRefChanges(hosting_path, logger=log))
62 self.repository.fetchRefCommits(
63 hosting_path, refs_to_upsert, logger=log)
64+ # The webhook delivery includes old ref information, so
65+ # prepare it before we actually execute the changes.
66+ if getFeatureFlag('code.git.webhooks.enabled'):
67+ payload = self.composeWebhookPayload(
68+ self.repository, refs_to_upsert, refs_to_remove)
69+ getUtility(IWebhookSet).trigger(
70+ self.repository, 'git:push:0.1', payload)
71 self.repository.synchroniseRefs(
72 refs_to_upsert, refs_to_remove, logger=log)
73 props = getUtility(IGitHostingClient).getProperties(
74
75=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
76--- lib/lp/code/model/tests/test_gitjob.py 2015-07-08 16:05:11 +0000
77+++ lib/lp/code/model/tests/test_gitjob.py 2015-08-10 12:52:50 +0000
78@@ -13,6 +13,8 @@
79
80 import pytz
81 from testtools.matchers import (
82+ Equals,
83+ MatchesDict,
84 MatchesSetwise,
85 MatchesStructure,
86 )
87@@ -34,6 +36,7 @@
88 ReclaimGitRepositorySpaceJob,
89 )
90 from lp.services.database.constants import UTC_NOW
91+from lp.services.features.testing import FeatureFixture
92 from lp.services.job.runner import JobRunner
93 from lp.testing import (
94 TestCaseWithFactory,
95@@ -178,6 +181,68 @@
96 JobRunner([job]).runAll()
97 self.assertEqual([], list(repository.refs))
98
99+ def test_triggers_webhooks(self):
100+ # Jobs trigger any relevant webhooks when they're enabled.
101+ self.useFixture(FeatureFixture({'code.git.webhooks.enabled': 'on'}))
102+ repository = self.factory.makeGitRepository()
103+ self.factory.makeGitRefs(
104+ repository, paths=[u'refs/heads/master', u'refs/tags/1.0'])
105+ hook = self.factory.makeWebhook(
106+ target=repository, event_types=['git:push:0.1'])
107+ job = GitRefScanJob.create(repository)
108+ paths = (u'refs/heads/master', u'refs/tags/2.0')
109+ hosting_client = FakeGitHostingClient(self.makeFakeRefs(paths), [])
110+ self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
111+ with dbuser('branchscanner'):
112+ JobRunner([job]).runAll()
113+ delivery = hook.deliveries.one()
114+ sha1 = lambda s: hashlib.sha1(s).hexdigest()
115+ self.assertThat(
116+ delivery,
117+ MatchesStructure(
118+ event_type=Equals('git:push:0.1'),
119+ payload=MatchesDict({
120+ 'git_repository_path': Equals(repository.unique_name),
121+ 'ref_changes': Equals({
122+ 'refs/tags/1.0': {
123+ 'old': {'commit_sha1': sha1('refs/tags/1.0')},
124+ 'new': None},
125+ 'refs/tags/2.0': {
126+ 'old': None,
127+ 'new': {'commit_sha1': sha1('refs/tags/2.0')}},
128+ })})))
129+
130+ def test_composeWebhookPayload(self):
131+ repository = self.factory.makeGitRepository()
132+ self.factory.makeGitRefs(
133+ repository, paths=[u'refs/heads/master', u'refs/tags/1.0'])
134+
135+ sha1 = lambda s: hashlib.sha1(s).hexdigest()
136+ new_refs = {
137+ 'refs/heads/master': {
138+ 'sha1': sha1('master-ng'),
139+ 'type': 'commit'},
140+ 'refs/tags/2.0': {
141+ 'sha1': sha1('2.0'),
142+ 'type': 'commit'},
143+ }
144+ removed_refs = ['refs/tags/1.0']
145+ payload = GitRefScanJob.composeWebhookPayload(
146+ repository, new_refs, removed_refs)
147+ self.assertEqual(
148+ {'git_repository_path': repository.unique_name,
149+ 'ref_changes': {
150+ 'refs/heads/master': {
151+ 'old': {'commit_sha1': sha1('refs/heads/master')},
152+ 'new': {'commit_sha1': sha1('master-ng')}},
153+ 'refs/tags/1.0': {
154+ 'old': {'commit_sha1': sha1('refs/tags/1.0')},
155+ 'new': None},
156+ 'refs/tags/2.0': {
157+ 'old': None,
158+ 'new': {'commit_sha1': sha1('2.0')}}}},
159+ payload)
160+
161
162 class TestReclaimGitRepositorySpaceJob(TestCaseWithFactory):
163 """Tests for `ReclaimGitRepositorySpaceJob`."""