├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── collaborators.ini
├── config.sample
├── eventhandler.py
├── handlers
├── easy_info
│ ├── __init__.py
│ └── tests
│ │ ├── assign_already_assigned.json
│ │ ├── assign_new.json
│ │ ├── label_easy.json
│ │ └── label_not_easy.json
├── empty_title_element
│ ├── __init__.py
│ └── tests
│ │ └── new_pr.json
├── homu_status
│ ├── __init__.py
│ └── tests
│ │ ├── json
│ │ └── builders
│ │ │ ├── test_builder_result.json
│ │ │ └── test_stdio
│ │ │ └── text
│ │ ├── merge_approved.json
│ │ ├── merge_conflict.json
│ │ ├── post_retry.json
│ │ └── tests_failed.json
├── label_watchers
│ ├── __init__.py
│ ├── tests
│ │ ├── dont_annoy.json
│ │ ├── dont_label_sender.json
│ │ ├── dont_notify_creator.json
│ │ ├── new_pr.json
│ │ └── not_watched.json
│ └── watchers.ini
├── missing_test
│ ├── __init__.py
│ └── tests
│ │ ├── new_pr.json
│ │ └── test_binding.json
├── no_modify_css_tests
│ ├── __init__.py
│ └── tests
│ │ └── new_pr.json
├── nonini_wpt_meta
│ ├── __init__.py
│ └── tests
│ │ ├── new_issue.json
│ │ └── new_pr.json
├── status_update
│ ├── __init__.py
│ ├── labels.ini
│ └── tests
│ │ ├── custom_labels.json
│ │ ├── dequeued.json
│ │ ├── dequeued_unmergeable.json
│ │ ├── enqueued.json
│ │ ├── merge.json
│ │ ├── new_pr.json
│ │ ├── new_pr_draft.json
│ │ ├── new_pr_unmergeable.json
│ │ ├── pr_ready_for_review.json
│ │ ├── review_approved.json
│ │ ├── review_changes.json
│ │ ├── synchronize.json
│ │ ├── synchronize_changes.json
│ │ ├── synchronize_draft.json
│ │ ├── synchronize_mergeable.json
│ │ ├── synchronize_not_mergeable.json
│ │ └── unmergeable_pr_ready_for_review.json
├── unsafe
│ ├── __init__.py
│ └── tests
│ │ └── new_pr.json
└── watchers
│ ├── __init__.py
│ ├── tests
│ ├── new_pr_author_is_not_watcher.json
│ ├── new_pr_author_is_watcher.json
│ ├── new_pr_wildcards_author_is_not_watcher.json
│ └── new_pr_wildcards_author_is_watcher.json
│ └── watchers.ini
├── helpers.py
├── json_cleanup.py
├── newpr.py
├── requirements.txt
└── test.py
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | types: ['opened', 'synchronize']
8 | branches: ["**"]
9 | workflow_dispatch:
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v5
16 | - name: Setup Python
17 | uses: actions/setup-python@v6
18 | - name: Install requirements
19 | run: pip install flake8 legacy-cgi
20 | - name: flake8
21 | run: flake8 .
22 | - name: tests
23 | run: python test.py
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | script:
5 | - flake8 .
6 | - python test.py
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Highfive
2 | ========
3 |
4 | GitHub hooks to provide an encouraging atmosphere for new contributors.
5 |
6 | Docs for the highfive instance for servo/servo repository live [on the Servo
7 | wiki](https://github.com/servo/servo/wiki/Highfive).
8 |
9 | ## Design
10 |
11 | Highfive is built as a modular, loosely-coupled set of handlers for Github
12 | API events. Each time an API event is processed, each handler is given the
13 | opportunity to respond to it, either by making direct API calls (such as
14 | manipulating PR labels) or using cross-handler features such as logging a
15 | warning (which are aggregated at the end and posted as a single comment).
16 |
17 | ## Testing
18 |
19 | Per-handler tests can be run using `python test.py`. These consist of
20 | a set of JSON documents collected from the `tests/` subdirectory of
21 | each handler, using the following format:
22 |
23 | ```js
24 | {
25 | "initial": {
26 | // Initial state of the PR before any handlers process the payload.
27 | "labels": [],
28 | "diff": "",
29 | "new_contributor": false,
30 | "assignee": null
31 | },
32 | "expected": {
33 | // Expected state of the PR after all the handlers process the following payload.
34 | // Only fields in this object will be checked. Example fields are shown below.
35 | "comments": 5,
36 | "labels": ["S-awaiting-review"],
37 | "assignee": "jdm"
38 | },
39 | "payload": {
40 | // Github API event payload in JSON format.
41 | }
42 | }
43 | ```
44 |
45 | Each test runs with a mock Github API provider, so no account information
46 | or network connection is required to run the test suite.
47 |
48 | ## Enabling a repo
49 |
50 | Visit the repo's webhook settings page at
51 | `https://github.com/org/repo/settings/hooks`.
52 |
53 | Create a new webhook, pointing at your highfive instance's location:
54 |
55 | Payload URL: `http://99.88.777.666/highfive/newpr.py`
56 | Content type: `application/x-www-form-urlencoded`
57 | Leave the 'secret' field blank.
58 | Let me select individual events: Issue Comment, Pull Request, Status
59 | Check the box by 'Active'
60 |
61 | Add the bot's github account as a Collaborator to the repo with Write access.
62 | If automatically assigning a reviewer is desired for the repo, add a new section
63 | to the highfive instance's `collaborators.ini` with the list of desired reviewers:
64 |
65 | ```
66 | [user/repo]
67 | reviewer_name =
68 | another_reviewer_name =
69 | ```
70 |
71 | ## Configuring a Highfive
72 |
73 | Copy `config.sample` to `config`. Add the username of the account that will be
74 | commenting as highfive. When logged into that account, visit
75 | `https://github.com/settings/tokens` and create a token with the `public_repo`
76 | permission.
77 |
78 | Add that access token's value to the `token` field of the config.
79 |
--------------------------------------------------------------------------------
/collaborators.ini:
--------------------------------------------------------------------------------
1 | [servo/servo-ignored]
2 | asajeffrey =
3 | jdm =
4 | Manishearth =
5 | nox =
6 | ferjm =
7 | SimonSapin =
8 |
9 | [servo/ipc-channel]
10 | emilio =
11 | jdm =
12 |
13 | [servo/highfive]
14 | jdm =
15 | Wafflespeanut =
16 |
17 | [servo/test-repo]
18 | highfive =
19 |
20 | [servo/test-repo2]
21 | highfive =
22 | highfive2 =
23 |
--------------------------------------------------------------------------------
/config.sample:
--------------------------------------------------------------------------------
1 | [github]
2 | user=highfive
3 | token=abc123xyz789
4 |
--------------------------------------------------------------------------------
/eventhandler.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from helpers import linear_search
3 |
4 | import importlib.util
5 | import os
6 | import sys
7 |
8 | _warnings = []
9 | _payload_actions = {
10 | 'opened': 'on_pr_opened',
11 | 'synchronize': 'on_pr_updated',
12 | 'created': 'on_new_comment',
13 | 'closed': 'on_pr_closed',
14 | 'labeled': 'on_issue_labeled',
15 | 'enqueued': 'on_pr_enqueued',
16 | 'dequeued': 'on_pr_dequeued',
17 | 'ready_for_review': 'on_pr_ready_for_review',
18 | 'submitted': 'on_review_submitted',
19 | }
20 |
21 |
22 | class EventHandler:
23 | def on_pr_opened(self, api, payload):
24 | pass
25 |
26 | def on_pr_updated(self, api, payload):
27 | pass
28 |
29 | def on_new_comment(self, api, payload):
30 | pass
31 |
32 | def on_pr_closed(self, api, payload):
33 | pass
34 |
35 | def on_pr_enqueued(self, api, payload):
36 | pass
37 |
38 | def on_pr_dequeued(self, api, payload):
39 | pass
40 |
41 | def on_pr_ready_for_review(self, api, payload):
42 | pass
43 |
44 | def on_issue_labeled(self, api, payload):
45 | pass
46 |
47 | def on_review_submitted(self, api, payload):
48 | pass
49 |
50 | def handle_payload(self, api, payload):
51 | def callback(action):
52 | if "pull_request" in payload and payload["pull_request"]["draft"] == True: # noqa
53 | return
54 | getattr(self, _payload_actions[action])(api, payload)
55 | payload_action = payload['action']
56 | linear_search(_payload_actions, payload_action, callback)
57 |
58 | def warn(self, msg):
59 | global _warnings
60 | _warnings += [msg]
61 |
62 | def is_open_pr(self, payload):
63 | return (payload['issue']['state'] == 'open' and
64 | 'pull_request' in payload['issue'])
65 |
66 |
67 | def reset_test_state():
68 | global _warnings
69 | _warnings = []
70 |
71 |
72 | def get_warnings():
73 | return _warnings
74 |
75 |
76 | def get_handlers():
77 | modules = []
78 | handlers = []
79 | possible_handlers = os.listdir('handlers')
80 | for i in possible_handlers:
81 | location = os.path.join('handlers', i, "__init__.py")
82 | abs_location = os.path.join(os.path.dirname(__file__), location)
83 | spec = importlib.util.spec_from_file_location(i, abs_location)
84 | if spec is None:
85 | raise ImportError(
86 | f"Could not load spec for module '{i}' at: {abs_location}"
87 | )
88 | module = importlib.util.module_from_spec(spec)
89 | sys.modules[i] = module
90 | spec.loader.exec_module(module)
91 | handlers.append(module.handler_interface())
92 | modules.append((module, location))
93 | return (modules, handlers)
94 |
--------------------------------------------------------------------------------
/handlers/easy_info/__init__.py:
--------------------------------------------------------------------------------
1 | from eventhandler import EventHandler
2 |
3 | import re
4 |
5 | ASSIGN_MSG = 'assign me'
6 |
7 | MSG = ('Hi! If you have any questions regarding this issue, feel free to make'
8 | ' a comment here, or ask it in '
9 | '[Zulip](https://servo.zulipchat.com/).\n\n'
10 | 'If you intend to work on this issue, then add `@%s: %s`'
11 | ' to your comment, and I\'ll assign this to you. :smile:')
12 |
13 | RESPONSE_FAIL = ('It looks like this has already been assigned to someone.'
14 | ' I\'ll leave the decision to a core contributor.')
15 |
16 | RESPONSE_OK = ('Hey @%s! Thanks for your interest in working on this issue.'
17 | ' It\'s now assigned to you!')
18 |
19 |
20 | class EasyInfoHandler(EventHandler):
21 | def on_issue_labeled(self, api, payload):
22 | if payload['label']['name'].lower() == 'e-easy':
23 | api.post_comment(MSG % (api.user, ASSIGN_MSG))
24 |
25 | def on_new_comment(self, api, payload):
26 | if payload['issue']['state'] != 'open':
27 | return
28 |
29 | user = payload['comment']['user']['login']
30 | if user == api.user: # ignore comments from self
31 | return # (since `MSG` already has `ASSIGN_MSG`)
32 |
33 | msg = payload['comment']['body']
34 |
35 | if re.search(r'@%s[: ]*%s' % (api.user, ASSIGN_MSG), str(msg)):
36 | labels = payload['issue']['labels']
37 | if any(label['name'] == 'C-assigned' for label in labels):
38 | api.post_comment(RESPONSE_FAIL)
39 | return
40 |
41 | api.add_label('C-assigned')
42 | api.set_assignee(user)
43 | api.post_comment(RESPONSE_OK % user)
44 |
45 |
46 | handler_interface = EasyInfoHandler
47 |
--------------------------------------------------------------------------------
/handlers/easy_info/tests/assign_already_assigned.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 1,
4 | "labels": [
5 | "C-assigned"
6 | ]
7 | },
8 | "initial": {
9 | "labels": [
10 | "C-assigned"
11 | ]
12 | },
13 | "payload": {
14 | "comment": {
15 | "body": "@highfive: assign me",
16 | "user": {
17 | "login": "someone"
18 | }
19 | },
20 | "repository": {
21 | "owner": {
22 | "login": "servo"
23 | },
24 | "name": "servo"
25 | },
26 | "action": "created",
27 | "issue": {
28 | "number": 7075,
29 | "state": "open",
30 | "labels": [
31 | {
32 | "name": "C-assigned"
33 | }
34 | ]
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/handlers/easy_info/tests/assign_new.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 1,
4 | "labels": [
5 | "C-assigned"
6 | ]
7 | },
8 | "initial": {},
9 | "payload": {
10 | "comment": {
11 | "body": "@highfive: assign me",
12 | "user": {
13 | "login": "someone"
14 | }
15 | },
16 | "repository": {
17 | "owner": {
18 | "login": "servo"
19 | },
20 | "name": "servo"
21 | },
22 | "action": "created",
23 | "issue": {
24 | "number": 7075,
25 | "state": "open",
26 | "labels": []
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/handlers/easy_info/tests/label_easy.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial": {},
3 | "expected": {
4 | "comments": 1
5 | },
6 | "payload": {
7 | "repository": {
8 | "owner": {
9 | "login": "servo"
10 | },
11 | "name": "highfive"
12 | },
13 | "label": {
14 | "name": "E-Easy"
15 | },
16 | "action": "labeled",
17 | "issue": {
18 | "number": 130
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/handlers/easy_info/tests/label_not_easy.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial": {},
3 | "expected": {
4 | "comments": 0
5 | },
6 | "payload": {
7 | "repository": {
8 | "owner": {
9 | "login": "servo"
10 | },
11 | "name": "highfive"
12 | },
13 | "label": {
14 | "name": "unrelated-label"
15 | },
16 | "action": "labeled",
17 | "issue": {
18 | "number": 130
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/handlers/empty_title_element/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from eventhandler import EventHandler
3 |
4 | WARNING = ("These commits include an empty title element (`
`). "
5 | "Consider adding appropriate metadata.")
6 |
7 |
8 | class EmptyTitleElementHandler(EventHandler):
9 | def on_pr_opened(self, api, payload):
10 | for line in api.get_added_lines():
11 | if line.find("") > -1:
12 | # This test doesn't consider case and whitespace the same way
13 | # that a HTML parser does, so empty title elements might still
14 | # go unnoticed. It will catch the low-hanging fruit, though.
15 | self.warn(WARNING)
16 | return
17 |
18 |
19 | handler_interface = EmptyTitleElementHandler
20 |
--------------------------------------------------------------------------------
/handlers/empty_title_element/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 1
4 | },
5 | "initial": {
6 | "diff": "+ "
7 | },
8 | "payload": {
9 | "number": 7076,
10 | "pull_request": {
11 | "draft": false,
12 | "base": {
13 | "repo": {
14 | "owner": {
15 | "login": "servo"
16 | },
17 | "name": "servo"
18 | }
19 | }
20 | },
21 | "action": "opened"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/handlers/homu_status/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import json
4 | import re
5 |
6 | from eventhandler import EventHandler
7 |
8 |
9 | def check_failure_log(api, bors_comment):
10 | # bors_comment would be something like,
11 | # ":broken_heart: Test failed - [linux2](https://build.servo.org/builders/linux2/builds/2627)" # noqa
12 | # ... from which we get the relevant build result url
13 | url = next(iter(re.findall(r'.*\((.*)\)', str(bors_comment))))
14 | if not url:
15 | return
16 |
17 | # Substitute and get the new url
18 | # (e.g. https://build.servo.org/json/builders/linux2/builds/2627)
19 | json_url = re.sub(r'(.*)(builders/.*)', r'\1json/\2', url)
20 | json_stuff = api.get_page_content(json_url)
21 | if not json_stuff:
22 | return
23 |
24 | build_stats = json.loads(json_stuff)
25 |
26 | build_log = []
27 | for step in build_stats['steps']:
28 | if 'failed' in step['text']:
29 | build_log = step['logs']
30 | break
31 |
32 | failed_tests_url = None
33 | failed_summary_url = None
34 | for (name, log_url) in build_log:
35 | if name == 'stdio':
36 | failed_tests_url = log_url + '/text'
37 | elif 'errorsummary.log' in name:
38 | failed_summary_url = log_url + '/text'
39 |
40 | if not failed_summary_url and not failed_tests_url:
41 | return
42 |
43 | failures = None
44 | if failed_tests_url:
45 | stdio = api.get_page_content(failed_tests_url)
46 | failure_regex = r'.*Tests with unexpected results:\n(.*)$'
47 | failures = next(iter(re.findall(failure_regex, stdio, re.DOTALL)))
48 |
49 | if not failures and failed_summary_url:
50 | failures = api.get_page_content(failed_summary_url)
51 |
52 | if failures:
53 | comments = ["Test failures:", ""]
54 | comments += [' ' * 4 + line for line in failures.split('\n')]
55 | comments += [" "]
56 | api.post_comment('\n'.join(comments))
57 |
58 |
59 | class HomuStatusHandler(EventHandler):
60 | def on_new_comment(self, api, payload):
61 | if not self.is_open_pr(payload):
62 | return
63 |
64 | if payload['comment']['user']['login'] != 'bors-servo':
65 | return
66 |
67 | labels = api.get_labels()
68 | msg = payload["comment"]["body"]
69 |
70 | def remove_if_exists(label):
71 | if label in labels:
72 | api.remove_label(label)
73 |
74 | if 'has been approved by' in msg or 'Testing commit' in msg:
75 | for label in ["S-awaiting-review", "S-needs-rebase",
76 | "S-tests-failed", "S-needs-code-changes",
77 | "S-needs-squash", "S-awaiting-answer"]:
78 | remove_if_exists(label)
79 | if "S-awaiting-merge" not in labels:
80 | api.add_label("S-awaiting-merge")
81 |
82 | elif 'Test failed' in msg:
83 | remove_if_exists("S-awaiting-merge")
84 | api.add_label("S-tests-failed")
85 | # Get the homu build stats url,
86 | # extract the failed tests and post them!
87 | check_failure_log(api, msg)
88 |
89 | elif 'Please resolve the merge conflicts' in msg:
90 | remove_if_exists("S-awaiting-merge")
91 | api.add_label("S-needs-rebase")
92 |
93 |
94 | handler_interface = HomuStatusHandler
95 |
--------------------------------------------------------------------------------
/handlers/homu_status/tests/json/builders/test_builder_result.json:
--------------------------------------------------------------------------------
1 | {
2 | "steps": [{
3 | "logs": [
4 | ["stdio", "handlers/homu_status/tests/json/builders/test_stdio"]
5 | ],
6 | "text": ["test", "failed"]
7 | }]
8 | }
9 |
--------------------------------------------------------------------------------
/handlers/homu_status/tests/json/builders/test_stdio/text:
--------------------------------------------------------------------------------
1 | // fooooooooooooo barrrrrrrrrrrrrrrrrrr
2 |
3 | Tests with unexpected results:
4 | ▶ OK [expected CRASH] /something/foo/something.html
5 |
6 | ▶ Unexpected subtest result in /something/blah/something.html
7 | │ FAIL [expected PASS] totally-something-else
8 | │
9 | │ reporting...
10 | └ I won't report anymore...
11 |
--------------------------------------------------------------------------------
/handlers/homu_status/tests/merge_approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-merge"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-needs-code-changes",
10 | "S-needs-rebase",
11 | "S-tests-failed",
12 | "S-needs-squash",
13 | "S-awaiting-review"
14 | ]
15 | },
16 | "payload": {
17 | "comment": {
18 | "body": ":pushpin: Commit 90d7d11 has been approved by `larsbergstrom+dzbarsky`\n\n",
19 | "user": {
20 | "login": "bors-servo"
21 | }
22 | },
23 | "repository": {
24 | "owner": {
25 | "login": "servo"
26 | },
27 | "name": "servo"
28 | },
29 | "action": "created",
30 | "issue": {
31 | "number": 7075,
32 | "state": "open",
33 | "pull_request": {}
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/handlers/homu_status/tests/merge_conflict.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-needs-rebase"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-awaiting-merge"
10 | ]
11 | },
12 | "payload": {
13 | "comment": {
14 | "body": ":umbrella: The latest upstream changes (presumably #6741) made this pull request unmergeable. Please resolve the merge conflicts.",
15 | "user": {
16 | "login": "bors-servo"
17 | }
18 | },
19 | "repository": {
20 | "owner": {
21 | "login": "servo"
22 | },
23 | "name": "servo"
24 | },
25 | "action": "created",
26 | "issue": {
27 | "number": 7075,
28 | "state": "open",
29 | "pull_request": {}
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/handlers/homu_status/tests/post_retry.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "labels": [
5 | "S-awaiting-merge"
6 | ]
7 | },
8 | {
9 | "labels": [
10 | "S-awaiting-merge"
11 | ]
12 | }
13 | ],
14 | "initial": [
15 | {
16 | "labels": [
17 | "S-tests-failed"
18 | ]
19 | },
20 | {
21 | "labels": [
22 | "S-awaiting-merge"
23 | ]
24 | }
25 | ],
26 | "payload": {
27 | "comment": {
28 | "body": ":hourglass: Testing commit 926cb90 with merge abf7399...",
29 | "user": {
30 | "login": "bors-servo"
31 | }
32 | },
33 | "repository": {
34 | "owner": {
35 | "login": "servo"
36 | },
37 | "name": "servo"
38 | },
39 | "action": "created",
40 | "issue": {
41 | "number": 7105,
42 | "state": "open",
43 | "pull_request": {}
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/handlers/homu_status/tests/tests_failed.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-tests-failed"
5 | ],
6 | "comments": 1
7 | },
8 | "initial": {
9 | "labels": [
10 | "S-awaiting-merge"
11 | ]
12 | },
13 | "payload": {
14 | "comment": {
15 | "body": ":broken_heart: Test failed - [linux2](handlers/homu_status/tests/builders/test_builder_result.json)",
16 | "user": {
17 | "login": "bors-servo"
18 | }
19 | },
20 | "repository": {
21 | "owner": {
22 | "login": "servo"
23 | },
24 | "name": "servo"
25 | },
26 | "action": "created",
27 | "issue": {
28 | "number": 7075,
29 | "state": "open",
30 | "pull_request": {}
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/handlers/label_watchers/__init__.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 | from eventhandler import EventHandler
3 | from helpers import get_people_from_config
4 |
5 | import os
6 |
7 | LABEL_WATCHERS_CONFIG_FILE = os.path.join(os.path.dirname(__file__),
8 | 'watchers.ini')
9 |
10 |
11 | def build_label_message(mentions):
12 | message = ['cc']
13 | for watcher in mentions:
14 | message.append("@{}".format(watcher))
15 |
16 | return ' '.join(message)
17 |
18 |
19 | class LabelWatchersHandler(EventHandler):
20 | def on_issue_labeled(self, api, payload):
21 | label_list = get_people_from_config(api, LABEL_WATCHERS_CONFIG_FILE)
22 | if not label_list:
23 | return
24 |
25 | new_label = payload['label']['name']
26 | existing_labels = []
27 | if 'issue' in payload:
28 | for label in payload['issue']['labels']:
29 | if new_label != label['name']:
30 | existing_labels.append(label['name'])
31 |
32 | creator = None
33 | sender = payload['sender']['login'].lower()
34 |
35 | if 'issue' in payload:
36 | creator = payload['issue']['user']['login'].lower()
37 | elif 'pull_request' in payload:
38 | creator = payload['pull_request']['user']['login'].lower()
39 |
40 | label_map = dict()
41 | for watcher, watched_labels in label_list: # reverse map
42 | if watcher == sender or watcher == creator:
43 | continue
44 |
45 | for label in watched_labels.split(' '):
46 | label_map.setdefault(label, set())
47 | label_map[label].add(watcher)
48 |
49 | mentions = deepcopy(label_map.get(new_label, set()))
50 | for label in existing_labels:
51 | for watcher in label_map.get(label, set()):
52 | if watcher in mentions: # avoid cc'ing again
53 | mentions.remove(watcher)
54 |
55 | if not mentions:
56 | return
57 |
58 | message = build_label_message(mentions)
59 | api.post_comment(message)
60 |
61 |
62 | handler_interface = LabelWatchersHandler
63 |
--------------------------------------------------------------------------------
/handlers/label_watchers/tests/dont_annoy.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 0
4 | },
5 | "initial": {},
6 | "payload": {
7 | "action": "labeled",
8 | "issue": {
9 | "labels": [
10 | {
11 | "name": "foo"
12 | },
13 | {
14 | "name": "bar"
15 | }
16 | ],
17 | "number": 16179,
18 | "user": {
19 | "login": "jdm"
20 | }
21 | },
22 | "sender": {
23 | "login": "jdm"
24 | },
25 | "repository": {
26 | "owner": {
27 | "login": "servo"
28 | },
29 | "name": "highfive-test"
30 | },
31 | "label": {
32 | "name": "foo"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/handlers/label_watchers/tests/dont_label_sender.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 0
4 | },
5 | "initial": {},
6 | "payload": {
7 | "sender": {
8 | "login": "jdm"
9 | },
10 | "repository": {
11 | "owner": {
12 | "login": "servo"
13 | },
14 | "name": "highfive"
15 | },
16 | "label": {
17 | "name": "enhancement"
18 | },
19 | "action": "labeled",
20 | "issue": {
21 | "labels": [{
22 | "name": "enhancement"
23 | }],
24 | "number": 84,
25 | "user": {
26 | "login": "nox"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/label_watchers/tests/dont_notify_creator.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 0
4 | },
5 | "initial": {},
6 | "payload": {
7 | "issue": {
8 | "number": 123,
9 | "labels": [{
10 | "name": "L-python"
11 | }],
12 | "user": {
13 | "login": "Wafflespeanut"
14 | }
15 | },
16 | "sender": {
17 | "login": "jdm"
18 | },
19 | "repository": {
20 | "owner": {
21 | "login": "servo"
22 | },
23 | "name": "servo"
24 | },
25 | "label": {
26 | "name": "L-python"
27 | },
28 | "action": "labeled"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/label_watchers/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 1
4 | },
5 | "initial": {},
6 | "payload": {
7 | "sender": {
8 | "login": "test"
9 | },
10 | "repository": {
11 | "owner": {
12 | "login": "servo"
13 | },
14 | "name": "highfive"
15 | },
16 | "label": {
17 | "name": "enhancement"
18 | },
19 | "action": "labeled",
20 | "issue": {
21 | "number": 84,
22 | "labels": [{
23 | "name": "enhancement"
24 | }],
25 | "user": {
26 | "login": "highfive"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/label_watchers/tests/not_watched.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 0
4 | },
5 | "initial": {},
6 | "payload": {
7 | "sender": {
8 | "login": "jdm"
9 | },
10 | "repository": {
11 | "owner": {
12 | "login": "servo"
13 | },
14 | "name": "highfive"
15 | },
16 | "label": {
17 | "name": "not-watched-label"
18 | },
19 | "action": "labeled",
20 | "issue": {
21 | "number": 84,
22 | "labels": [{
23 | "name": "not-watched-label"
24 | }],
25 | "user": {
26 | "login": "Wafflespeanut"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/label_watchers/watchers.ini:
--------------------------------------------------------------------------------
1 | [servo/servo]
2 | emilio = A-stylo
3 |
4 | [servo/highfive]
5 | jdm = enhancement
6 |
7 | [servo/highfive-test]
8 | test_user = foo bar
9 |
10 | [mozillareality/firefoxreality]
11 | thenadj = Design
12 |
--------------------------------------------------------------------------------
/handlers/missing_test/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from eventhandler import EventHandler
4 |
5 | TEST_REQUIRED_MSG = ('These commits modify {} code, but no tests are modified.'
6 | ' Please consider adding a test!')
7 |
8 |
9 | class MissingTestHandler(EventHandler):
10 | COMPONENT_DIRS_TO_CHECK = ('layout', 'script', 'gfx', 'style', 'net')
11 | TEST_DIRS_TO_CHECK = ('ref', 'wpt', 'unit',
12 | 'compiletest/plugin/compile-fail')
13 | TEST_FILES_TO_CHECK = [
14 | '{0}/{1}'.format('components/script/dom', test_file)
15 | for test_file in ['testbinding.rs',
16 | 'webidls/TestBinding.webidl',
17 | 'testbindingproxy.rs',
18 | 'webidls/TestBindingProxy.webidl',
19 | 'testbindingiterable.rs',
20 | 'webidls/TestBindingIterable.webidl',
21 | 'testbindingpairiterable.rs',
22 | 'webidls/TestBindingPairIterable.webidl']
23 | ]
24 |
25 | def on_pr_opened(self, api, payload):
26 | return
27 | components_changed = set()
28 |
29 | for filepath in api.get_changed_files():
30 | for component in self.COMPONENT_DIRS_TO_CHECK:
31 | if 'components/{0}/'.format(component) in filepath:
32 | components_changed.add(component)
33 |
34 | if '/tests/' in filepath:
35 | return
36 |
37 | for directory in self.TEST_DIRS_TO_CHECK:
38 | if 'tests/{0}'.format(directory) in filepath:
39 | return
40 |
41 | for test_file in self.TEST_FILES_TO_CHECK:
42 | if test_file in filepath:
43 | return
44 |
45 | if components_changed:
46 | # Build a readable list of changed components
47 | if len(components_changed) == 1:
48 | components_msg = components_changed.pop()
49 | elif len(components_changed) == 2:
50 | components_msg = '{} and {}'.format(*components_changed)
51 | else:
52 | components_msg = ', '.join(components_changed)
53 | components_msg = ", and ".join(components_msg.rsplit(", ", 1))
54 |
55 | self.warn(TEST_REQUIRED_MSG.format(components_msg))
56 |
57 |
58 | handler_interface = MissingTestHandler
59 |
--------------------------------------------------------------------------------
/handlers/missing_test/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 1
5 | },
6 | {
7 | "comments": 1
8 | },
9 | {
10 | "comments": 1
11 | },
12 | {
13 | "comments": 1
14 | },
15 | {
16 | "comments": 1
17 | },
18 | {
19 | "comments": 0
20 | },
21 | {
22 | "comments": 0
23 | },
24 | {
25 | "comments": 0
26 | },
27 | {
28 | "comments": 0
29 | },
30 | {
31 | "comments": 0
32 | },
33 | {
34 | "comments": 0
35 | },
36 | {
37 | "comments": 0
38 | }
39 | ],
40 | "initial": [
41 | {
42 | "diff": "diff --git components/layout/"
43 | },
44 | {
45 | "diff": "diff --git components/script/"
46 | },
47 | {
48 | "diff": "diff --git components/gfx/"
49 | },
50 | {
51 | "diff": "diff --git components/style/"
52 | },
53 | {
54 | "diff": "diff --git components/net/"
55 | },
56 | {
57 | "diff": "diff --git components/layout/\ndiff --git tests/wpt"
58 | },
59 | {
60 | "diff": "diff --git components/gfx/\ndiff --git tests/unit"
61 | },
62 | {
63 | "diff": "diff --git components/style/\ndiff --git tests/unit"
64 | },
65 | {
66 | "diff": "diff --git components/net/\ndiff --git tests/unit"
67 | },
68 | {
69 | "diff": "diff --git components/script/\ndiff --git tests/unit"
70 | },
71 | {
72 | "diff": "diff --git components/script/\ndiff --git tests/compiletest/plugin/compile-fail"
73 | },
74 | {
75 | "diff": "diff --git components/net/blah.rs\ndiff --git components/net/tests/blah.rs"
76 | }
77 | ],
78 | "payload": {
79 | "number": 7076,
80 | "pull_request": {
81 | "draft": false,
82 | "base": {
83 | "repo": {
84 | "owner": {
85 | "login": "servo"
86 | },
87 | "name": "servo"
88 | }
89 | }
90 | },
91 | "action": "opened"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/handlers/missing_test/tests/test_binding.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial": [
3 | {
4 | "diff": "diff --git components/script/dom/testbinding.rs"
5 | },
6 | {
7 | "diff": "diff --git components/script/dom/webidls/TestBinding.webidl"
8 | },
9 | {
10 | "diff": "diff --git components/script/dom/testbindingproxy.rs"
11 | },
12 | {
13 | "diff": "diff --git components/script/dom/webidls/TestBindingProxy.webidl"
14 | },
15 | {
16 | "diff": "diff --git components/script/dom/testbindingiterable.rs"
17 | },
18 | {
19 | "diff": "diff --git components/script/dom/webidls/TestBindingIterable.webidl"
20 | },
21 | {
22 | "diff": "diff --git components/script/dom/testbindingpairiterable.rs"
23 | },
24 | {
25 | "diff": "diff --git components/script/dom/webidls/TestBindingPairIterable.webidl"
26 | }
27 | ],
28 | "expected": [
29 | {
30 | "comments": 0
31 | },
32 | {
33 | "comments": 0
34 | },
35 | {
36 | "comments": 0
37 | },
38 | {
39 | "comments": 0
40 | },
41 | {
42 | "comments": 0
43 | },
44 | {
45 | "comments": 0
46 | },
47 | {
48 | "comments": 0
49 | },
50 | {
51 | "comments": 0
52 | }
53 | ],
54 | "payload": {
55 | "number": 10355,
56 | "pull_request": {
57 | "draft": false,
58 | "base": {
59 | "repo": {
60 | "owner": {
61 | "login": "servo"
62 | },
63 | "name": "servo"
64 | }
65 | }
66 | },
67 | "action": "opened"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/handlers/no_modify_css_tests/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from eventhandler import EventHandler
4 |
5 | NO_MODIFY_CSS_TESTS_MSG = '''This pull request modifies the contents of
6 | `tests/wpt/css-tests/`, which are overwriten occasionally whenever the
7 | directory is synced from upstream.'''
8 |
9 |
10 | class NoModifyCSSTestsHandler(EventHandler):
11 | DIR_TO_CHECK = "tests/wpt/css-tests"
12 |
13 | def on_pr_opened(self, api, payload):
14 | for filepath in api.get_changed_files():
15 | if self.DIR_TO_CHECK in filepath:
16 | self.warn(NO_MODIFY_CSS_TESTS_MSG)
17 | break
18 |
19 |
20 | handler_interface = NoModifyCSSTestsHandler
21 |
--------------------------------------------------------------------------------
/handlers/no_modify_css_tests/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 1
5 | },
6 | {
7 | "comments": 0
8 | },
9 | {
10 | "comments": 0
11 | },
12 | {
13 | "comments": 1
14 | },
15 | {
16 | "comments": 0
17 | }
18 | ],
19 | "initial": [
20 | {
21 | "diff": "diff --git tests/wpt/css-tests"
22 | },
23 | {
24 | "diff": "diff --git tests/"
25 | },
26 | {
27 | "diff": "diff --git ports/"
28 | },
29 | {
30 | "diff": "diff --git tests/wpt/css-tests/LICENSE"
31 | },
32 | {
33 | "diff": "not diff --git tests/wpt/css-tests/manifest"
34 | }
35 | ],
36 | "payload": {
37 | "number": 7076,
38 | "pull_request": {
39 | "draft": false,
40 | "base": {
41 | "repo": {
42 | "owner": {
43 | "login": "servo"
44 | },
45 | "name": "servo"
46 | }
47 | }
48 | },
49 | "action": "opened"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/handlers/nonini_wpt_meta/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from eventhandler import EventHandler
4 |
5 | NON_INI_MSG = 'This pull request adds {0} without the .ini \
6 | file extension to {1}. Please consider removing {2}!'
7 |
8 |
9 | class NonINIWPTMetaFileHandler(EventHandler):
10 | DIRS_TO_CHECK = (
11 | 'tests/wpt/meta',
12 | 'tests/wpt/mozilla/meta',
13 | )
14 |
15 | FALSE_POSITIVE_SUBSTRINGS = (
16 | '.ini',
17 | 'MANIFEST.json',
18 | 'mozilla-sync',
19 | )
20 |
21 | def _wpt_ini_dirs(self, line):
22 | if '.' in line and not any(fp in line
23 | for fp in self.FALSE_POSITIVE_SUBSTRINGS):
24 | return set(directory for directory in self.DIRS_TO_CHECK
25 | if directory in line)
26 | else:
27 | return set()
28 |
29 | def on_pr_opened(self, api, payload):
30 | test_dirs_with_offending_files = set()
31 |
32 | for filepath in api.get_changed_files():
33 | test_dirs_with_offending_files |= self._wpt_ini_dirs(filepath)
34 |
35 | if test_dirs_with_offending_files:
36 | if len(test_dirs_with_offending_files) == 1:
37 | files = "a file"
38 | test_dirs = test_dirs_with_offending_files.pop()
39 | remove = "it"
40 | else:
41 | files = "files"
42 | test_dirs = '{} and {}'.format(*test_dirs_with_offending_files)
43 | remove = "them"
44 |
45 | self.warn(NON_INI_MSG.format(files, test_dirs, remove))
46 |
47 |
48 | handler_interface = NonINIWPTMetaFileHandler
49 |
--------------------------------------------------------------------------------
/handlers/nonini_wpt_meta/tests/new_issue.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 0
5 | }
6 | ],
7 | "initial": [
8 | {}
9 | ],
10 | "payload": {
11 | "issue": {
12 | "number": 7076
13 | },
14 | "repository": {
15 | "owner": {
16 | "login": ""
17 | },
18 | "name": ""
19 | },
20 | "action": "opened"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/handlers/nonini_wpt_meta/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 0
5 | },
6 | {
7 | "comments": 1
8 | },
9 | {
10 | "comments": 1
11 | },
12 | {
13 | "comments": 0
14 | },
15 | {
16 | "comments": 0
17 | },
18 | {
19 | "comments": 0
20 | },
21 | {
22 | "comments": 0
23 | }
24 | ],
25 | "initial": [
26 | {
27 | "diff": "diff --git tests/wpt/meta/foo.ini"
28 | },
29 | {
30 | "diff": "diff --git tests/wpt/mozilla/meta/bar.tgz"
31 | },
32 | {
33 | "diff": "diff --git tests/wpt/mozilla/meta/bar.tgz tests/wpt/metadata/spam.xz"
34 | },
35 | {
36 | "diff": "diff --git tests/wpt/meta"
37 | },
38 | {
39 | "diff": "not diff tests/wpt/meta/eggs.delicious"
40 | },
41 | {
42 | "diff": "diff --git tests/wpt/meta/MANIFEST.json"
43 | },
44 | {
45 | "diff": "diff --git tests/wpt/meta/mozilla-sync"
46 | }
47 | ],
48 | "payload": {
49 | "number": 7076,
50 | "pull_request": {
51 | "draft": false,
52 | "base": {
53 | "repo": {
54 | "owner": {
55 | "login": "servo"
56 | },
57 | "name": "servo"
58 | }
59 | }
60 | },
61 | "action": "opened"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/handlers/status_update/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from configparser import ConfigParser
4 | import os.path
5 | import time
6 |
7 | from eventhandler import EventHandler
8 |
9 | AWAITING_MERGE = "S-awaiting-merge"
10 | AWAITING_REVIEW = "S-awaiting-review"
11 | NEED_CODE_CHANGES = "S-needs-code-changes"
12 | NEED_REBASE = "S-needs-rebase"
13 | TESTS_FAILED = "S-tests-failed"
14 |
15 | config = ConfigParser()
16 | config.optionxform = str # Be case sensitive
17 | config.read(os.path.join(os.path.dirname(__file__), 'labels.ini'))
18 | config = {
19 | repo: {
20 | event: set(labels.split(' ')) for event, labels in config.items(repo)
21 | }
22 | for repo in config.sections()
23 | }
24 | # TODO(aneeshusa): Add checking of config option validity
25 |
26 |
27 | def clear_pr_labels(api):
28 | labels = api.get_labels()
29 |
30 | for label in [
31 | AWAITING_MERGE,
32 | TESTS_FAILED,
33 | NEED_CODE_CHANGES,
34 | NEED_REBASE,
35 | ]:
36 | if label in labels:
37 | api.remove_label(label)
38 |
39 |
40 | def handle_custom_labels(api, event):
41 | repo_config = config.get('{}/{}'.format(api.owner, api.repo), None)
42 | if not repo_config:
43 | return
44 | labels = api.get_labels()
45 | for label in repo_config.get('remove_on_pr_{}'.format(event), []):
46 | if label in labels:
47 | api.remove_label(label)
48 | for label in repo_config.get('add_on_pr_{}'.format(event), []):
49 | if label not in labels:
50 | api.add_label(label)
51 |
52 |
53 | def update_rebase_status(api, payload):
54 | if "pull_request" not in payload:
55 | return
56 |
57 | mergeable = payload['pull_request'].get('mergeable', None)
58 |
59 | # If mergeable is null, the data wasn't available yet.
60 | # Once it is, mergeable will be either true or false.
61 | while mergeable == None: # noqa
62 | time.sleep(1) # wait for GitHub to finish determine mergeability
63 | pull_request = api.get_pull()
64 | mergeable = pull_request['mergeable']
65 |
66 | if mergeable == False: # noqa
67 | api.add_label(NEED_REBASE)
68 |
69 |
70 | def is_draft_pr(payload):
71 | return "pull_request" in payload and \
72 | payload["pull_request"]["draft"] == True # noqa
73 |
74 |
75 | class StatusUpdateHandler(EventHandler):
76 | def on_pr_opened(self, api, payload):
77 | if "pull_request" not in payload or is_draft_pr(payload):
78 | return
79 |
80 | labels = api.get_labels()
81 | if AWAITING_REVIEW not in labels:
82 | api.add_label(AWAITING_REVIEW)
83 | update_rebase_status(api, payload)
84 | handle_custom_labels(api, 'opened')
85 |
86 | def on_pr_updated(self, api, payload):
87 | if is_draft_pr(payload):
88 | return
89 |
90 | clear_pr_labels(api)
91 | api.add_label(AWAITING_REVIEW)
92 | update_rebase_status(api, payload)
93 | handle_custom_labels(api, 'updated')
94 |
95 | def on_pr_ready_for_review(self, api, payload):
96 | clear_pr_labels(api)
97 | api.add_label(AWAITING_REVIEW)
98 | update_rebase_status(api, payload)
99 | handle_custom_labels(api, 'ready')
100 |
101 | def on_pr_closed(self, api, payload):
102 | handle_custom_labels(api, 'closed')
103 | if "pull_request" in payload and \
104 | payload['pull_request']['merged'] == True: # noqa
105 | api.remove_label(AWAITING_MERGE)
106 | handle_custom_labels(api, 'merged')
107 |
108 | def on_pr_enqueued(self, api, payload):
109 | clear_pr_labels(api)
110 | api.add_label(AWAITING_MERGE)
111 | handle_custom_labels(api, 'enqueued')
112 |
113 | def on_pr_dequeued(self, api, payload):
114 | labels = api.get_labels()
115 | update_rebase_status(api, payload)
116 | if payload["reason"] != "MERGE":
117 | if AWAITING_MERGE in labels:
118 | api.remove_label(AWAITING_MERGE)
119 | if payload["reason"] == "CI_FAILURE":
120 | api.add_label(TESTS_FAILED)
121 | handle_custom_labels(api, 'dequeued')
122 |
123 | def on_review_submitted(self, api, payload):
124 | update_rebase_status(api, payload)
125 | labels = api.get_labels()
126 | state = payload["review"]["state"]
127 | if state == "changes_requested":
128 | if AWAITING_REVIEW in labels:
129 | api.remove_label(AWAITING_REVIEW)
130 | if NEED_CODE_CHANGES not in labels:
131 | api.add_label(NEED_CODE_CHANGES)
132 | elif state == "approved":
133 | if AWAITING_REVIEW in labels:
134 | api.remove_label(AWAITING_REVIEW)
135 |
136 |
137 | handler_interface = StatusUpdateHandler
138 |
--------------------------------------------------------------------------------
/handlers/status_update/labels.ini:
--------------------------------------------------------------------------------
1 | [servo/saltfs]
2 | add_on_pr_merged = S-needs-deploy
3 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/custom_labels.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-needs-deploy"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-awaiting-merge"
10 | ]
11 | },
12 | "payload": {
13 | "number": 659,
14 | "pull_request": {
15 | "draft": false,
16 | "base": {
17 | "repo": {
18 | "owner": {
19 | "login": "servo"
20 | },
21 | "name": "saltfs"
22 | }
23 | },
24 | "merged": true
25 | },
26 | "action": "closed"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/dequeued.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": ["S-tests-failed"]
4 | },
5 | "initial": {
6 | "labels": [
7 | "S-awaiting-merge"
8 | ]
9 | },
10 | "payload": {
11 | "number": 7062,
12 | "reason": "CI_FAILURE",
13 | "pull_request": {
14 | "draft": false,
15 | "mergeable": true,
16 | "base": {
17 | "repo": {
18 | "owner": {
19 | "login": "servo"
20 | },
21 | "name": "servo"
22 | }
23 | }
24 | },
25 | "action": "dequeued"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/dequeued_unmergeable.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": ["S-needs-rebase"]
4 | },
5 | "initial": {
6 | "labels": [
7 | "S-awaiting-merge"
8 | ]
9 | },
10 | "payload": {
11 | "number": 7062,
12 | "reason": "MERGE_CONFLICT",
13 | "pull_request": {
14 | "draft": false,
15 | "mergeable": false,
16 | "base": {
17 | "repo": {
18 | "owner": {
19 | "login": "servo"
20 | },
21 | "name": "servo"
22 | }
23 | }
24 | },
25 | "action": "dequeued"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/enqueued.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-merge"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-needs-code-changes",
10 | "S-tests-failed",
11 | "S-awaiting-merge",
12 | "S-needs-rebase"
13 | ]
14 | },
15 | "payload": {
16 | "number": 7062,
17 | "pull_request": {
18 | "draft": false,
19 | "base": {
20 | "repo": {
21 | "owner": {
22 | "login": "servo"
23 | },
24 | "name": "servo"
25 | }
26 | }
27 | },
28 | "action": "enqueued"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/merge.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": []
4 | },
5 | "initial": {
6 | "labels": [
7 | "S-awaiting-merge"
8 | ]
9 | },
10 | "payload": {
11 | "number": 10315,
12 | "pull_request": {
13 | "draft": false,
14 | "base": {
15 | "repo": {
16 | "owner": {
17 | "login": "servo"
18 | },
19 | "name": "servo"
20 | }
21 | },
22 | "merged": true
23 | },
24 | "action": "closed"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review"
5 | ]
6 | },
7 | "initial": {},
8 | "payload": {
9 | "number": 7076,
10 | "pull_request": {
11 | "draft": false,
12 | "mergeable": true,
13 | "base": {
14 | "repo": {
15 | "owner": {
16 | "login": "servo"
17 | },
18 | "name": "servo"
19 | }
20 | }
21 | },
22 | "action": "opened"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/new_pr_draft.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | ]
5 | },
6 | "initial": {},
7 | "payload": {
8 | "number": 7076,
9 | "pull_request": {
10 | "draft": true,
11 | "base": {
12 | "repo": {
13 | "owner": {
14 | "login": "servo"
15 | },
16 | "name": "servo"
17 | }
18 | }
19 | },
20 | "action": "opened"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/new_pr_unmergeable.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review",
5 | "S-needs-rebase"
6 | ]
7 | },
8 | "initial": {},
9 | "payload": {
10 | "number": 7076,
11 | "pull_request": {
12 | "draft": false,
13 | "mergeable": false,
14 | "base": {
15 | "repo": {
16 | "owner": {
17 | "login": "servo"
18 | },
19 | "name": "servo"
20 | }
21 | }
22 | },
23 | "action": "opened"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/pr_ready_for_review.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review"
5 | ]
6 | },
7 | "initial": {},
8 | "payload": {
9 | "number": 7076,
10 | "pull_request": {
11 | "draft": false,
12 | "mergeable": true,
13 | "base": {
14 | "repo": {
15 | "owner": {
16 | "login": "servo"
17 | },
18 | "name": "servo"
19 | }
20 | }
21 | },
22 | "action": "ready_for_review"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/review_approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | ]
5 | },
6 | "initial": {
7 | "labels": [
8 | "S-awaiting-review"
9 | ]
10 | },
11 | "payload": {
12 | "number": 7076,
13 | "review": {
14 | "state": "approved"
15 | },
16 | "pull_request": {
17 | "draft": false,
18 | "mergeable": true,
19 | "base": {
20 | "repo": {
21 | "owner": {
22 | "login": "servo"
23 | },
24 | "name": "servo"
25 | }
26 | }
27 | },
28 | "action": "submitted"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/review_changes.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-needs-code-changes"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-awaiting-review"
10 | ]
11 | },
12 | "payload": {
13 | "number": 7076,
14 | "review": {
15 | "state": "changes_requested"
16 | },
17 | "pull_request": {
18 | "draft": false,
19 | "mergeable": true,
20 | "base": {
21 | "repo": {
22 | "owner": {
23 | "login": "servo"
24 | },
25 | "name": "servo"
26 | }
27 | }
28 | },
29 | "action": "submitted"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/synchronize.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-needs-code-changes",
10 | "S-tests-failed",
11 | "S-awaiting-merge"
12 | ]
13 | },
14 | "payload": {
15 | "number": 7062,
16 | "pull_request": {
17 | "draft": false,
18 | "mergeable": true,
19 | "base": {
20 | "repo": {
21 | "owner": {
22 | "login": "servo"
23 | },
24 | "name": "servo"
25 | }
26 | }
27 | },
28 | "action": "synchronize"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/synchronize_changes.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 0
4 | },
5 | "initial": {},
6 | "payload": {
7 | "number": 7062,
8 | "pull_request": {
9 | "draft": false,
10 | "mergeable": true,
11 | "base": {
12 | "repo": {
13 | "owner": {
14 | "login": "servo"
15 | },
16 | "name": "servo"
17 | }
18 | }
19 | },
20 | "action": "synchronize"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/synchronize_draft.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-needs-code-changes",
5 | "S-tests-failed",
6 | "S-awaiting-merge"
7 | ]
8 | },
9 | "initial": {
10 | "labels": [
11 | "S-needs-code-changes",
12 | "S-tests-failed",
13 | "S-awaiting-merge"
14 | ]
15 | },
16 | "payload": {
17 | "number": 7062,
18 | "pull_request": {
19 | "draft": true,
20 | "base": {
21 | "repo": {
22 | "owner": {
23 | "login": "servo"
24 | },
25 | "name": "servo"
26 | }
27 | }
28 | },
29 | "action": "synchronize"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/synchronize_mergeable.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review"
5 | ]
6 | },
7 | "initial": {
8 | "labels": [
9 | "S-needs-rebase"
10 | ],
11 | "pull_request": {
12 | "body": "This excludes:\r\n\r\n* tenacious, as it has not yet been updated to the current rustc\r\n (Manishearth/rust-tenacious#6);\r\n* ipc-channel, as it doesn't build on linux upstream.\n\n\n[
](https://reviewable.io/reviews/servo/servo/7062)\n\n",
13 | "merge_commit_sha": "30f3785911255fde11ab91ef5c0a9fe741174aff",
14 | "number": 7062,
15 | "assignee": null,
16 | "mergeable": true,
17 | "closed_at": null,
18 | "additions": 483,
19 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/289b421490113278d323ef482d77a1cacb560ac7",
20 | "id": 41866663,
21 | "title": "Update most dependencies.",
22 | "comments": 3,
23 | "merged_at": null,
24 | "state": "open",
25 | "_links": {
26 | "review_comment": {
27 | "href": "https://api.github.com/repos/servo/servo/pulls/comments{/number}"
28 | },
29 | "commits": {
30 | "href": "https://api.github.com/repos/servo/servo/pulls/7062/commits"
31 | },
32 | "self": {
33 | "href": "https://api.github.com/repos/servo/servo/pulls/7062"
34 | },
35 | "comments": {
36 | "href": "https://api.github.com/repos/servo/servo/issues/7062/comments"
37 | },
38 | "html": {
39 | "href": "https://github.com/servo/servo/pull/7062"
40 | },
41 | "review_comments": {
42 | "href": "https://api.github.com/repos/servo/servo/pulls/7062/comments"
43 | },
44 | "issue": {
45 | "href": "https://api.github.com/repos/servo/servo/issues/7062"
46 | },
47 | "statuses": {
48 | "href": "https://api.github.com/repos/servo/servo/statuses/289b421490113278d323ef482d77a1cacb560ac7"
49 | }
50 | },
51 | "commits": 1,
52 | "diff_url": "https://github.com/servo/servo/pull/7062.diff",
53 | "issue_url": "https://api.github.com/repos/servo/servo/issues/7062",
54 | "patch_url": "https://github.com/servo/servo/pull/7062.patch",
55 | "deletions": 438,
56 | "head": {
57 | "repo": {
58 | "issues_url": "https://api.github.com/repos/servo/servo/issues{/number}",
59 | "has_wiki": true,
60 | "forks_url": "https://api.github.com/repos/servo/servo/forks",
61 | "mirror_url": null,
62 | "subscription_url": "https://api.github.com/repos/servo/servo/subscription",
63 | "merges_url": "https://api.github.com/repos/servo/servo/merges",
64 | "collaborators_url": "https://api.github.com/repos/servo/servo/collaborators{/collaborator}",
65 | "updated_at": "2015-08-07T09:37:49Z",
66 | "private": false,
67 | "pulls_url": "https://api.github.com/repos/servo/servo/pulls{/number}",
68 | "issue_comment_url": "https://api.github.com/repos/servo/servo/issues/comments{/number}",
69 | "full_name": "servo/servo",
70 | "owner": {
71 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
72 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
73 | "organizations_url": "https://api.github.com/users/servo/orgs",
74 | "url": "https://api.github.com/users/servo",
75 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
76 | "html_url": "https://github.com/servo",
77 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
78 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
79 | "repos_url": "https://api.github.com/users/servo/repos",
80 | "received_events_url": "https://api.github.com/users/servo/received_events",
81 | "gravatar_id": "",
82 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
83 | "site_admin": false,
84 | "login": "servo",
85 | "type": "Organization",
86 | "id": 2566135,
87 | "followers_url": "https://api.github.com/users/servo/followers"
88 | },
89 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/{sha}",
90 | "id": 3390243,
91 | "keys_url": "https://api.github.com/repos/servo/servo/keys{/key_id}",
92 | "size": 1903023,
93 | "tags_url": "https://api.github.com/repos/servo/servo/tags",
94 | "issue_events_url": "https://api.github.com/repos/servo/servo/issues/events{/number}",
95 | "contributors_url": "https://api.github.com/repos/servo/servo/contributors",
96 | "downloads_url": "https://api.github.com/repos/servo/servo/downloads",
97 | "has_downloads": true,
98 | "assignees_url": "https://api.github.com/repos/servo/servo/assignees{/user}",
99 | "contents_url": "https://api.github.com/repos/servo/servo/contents/{+path}",
100 | "has_pages": false,
101 | "git_refs_url": "https://api.github.com/repos/servo/servo/git/refs{/sha}",
102 | "clone_url": "https://github.com/servo/servo.git",
103 | "watchers_count": 4571,
104 | "git_tags_url": "https://api.github.com/repos/servo/servo/git/tags{/sha}",
105 | "milestones_url": "https://api.github.com/repos/servo/servo/milestones{/number}",
106 | "stargazers_count": 4571,
107 | "homepage": "",
108 | "branches_url": "https://api.github.com/repos/servo/servo/branches{/branch}",
109 | "fork": false,
110 | "commits_url": "https://api.github.com/repos/servo/servo/commits{/sha}",
111 | "releases_url": "https://api.github.com/repos/servo/servo/releases{/id}",
112 | "description": "The Servo Browser Engine",
113 | "archive_url": "https://api.github.com/repos/servo/servo/{archive_format}{/ref}",
114 | "labels_url": "https://api.github.com/repos/servo/servo/labels{/name}",
115 | "events_url": "https://api.github.com/repos/servo/servo/events",
116 | "comments_url": "https://api.github.com/repos/servo/servo/comments{/number}",
117 | "html_url": "https://github.com/servo/servo",
118 | "forks": 717,
119 | "compare_url": "https://api.github.com/repos/servo/servo/compare/{base}...{head}",
120 | "trees_url": "https://api.github.com/repos/servo/servo/git/trees{/sha}",
121 | "git_url": "git://github.com/servo/servo.git",
122 | "svn_url": "https://github.com/servo/servo",
123 | "notifications_url": "https://api.github.com/repos/servo/servo/notifications{?since,all,participating}",
124 | "has_issues": true,
125 | "ssh_url": "git@github.com:servo/servo.git",
126 | "blobs_url": "https://api.github.com/repos/servo/servo/git/blobs{/sha}",
127 | "languages_url": "https://api.github.com/repos/servo/servo/languages",
128 | "hooks_url": "https://api.github.com/repos/servo/servo/hooks",
129 | "open_issues_count": 1057,
130 | "watchers": 4571,
131 | "name": "servo",
132 | "language": "Rust",
133 | "url": "https://api.github.com/repos/servo/servo",
134 | "created_at": "2012-02-08T19:07:25Z",
135 | "pushed_at": "2015-08-07T16:09:10Z",
136 | "forks_count": 717,
137 | "default_branch": "master",
138 | "teams_url": "https://api.github.com/repos/servo/servo/teams",
139 | "open_issues": 1057,
140 | "git_commits_url": "https://api.github.com/repos/servo/servo/git/commits{/sha}",
141 | "subscribers_url": "https://api.github.com/repos/servo/servo/subscribers",
142 | "stargazers_url": "https://api.github.com/repos/servo/servo/stargazers"
143 | },
144 | "sha": "289b421490113278d323ef482d77a1cacb560ac7",
145 | "ref": "update-some",
146 | "user": {
147 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
148 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
149 | "organizations_url": "https://api.github.com/users/servo/orgs",
150 | "url": "https://api.github.com/users/servo",
151 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
152 | "html_url": "https://github.com/servo",
153 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
154 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
155 | "repos_url": "https://api.github.com/users/servo/repos",
156 | "received_events_url": "https://api.github.com/users/servo/received_events",
157 | "gravatar_id": "",
158 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
159 | "site_admin": false,
160 | "login": "servo",
161 | "type": "Organization",
162 | "id": 2566135,
163 | "followers_url": "https://api.github.com/users/servo/followers"
164 | },
165 | "label": "servo:update-some"
166 | },
167 | "commits_url": "https://api.github.com/repos/servo/servo/pulls/7062/commits",
168 | "changed_files": 3,
169 | "comments_url": "https://api.github.com/repos/servo/servo/issues/7062/comments",
170 | "html_url": "https://github.com/servo/servo/pull/7062",
171 | "updated_at": "2015-08-07T16:09:10Z",
172 | "base": {
173 | "repo": {
174 | "issues_url": "https://api.github.com/repos/servo/servo/issues{/number}",
175 | "has_wiki": true,
176 | "forks_url": "https://api.github.com/repos/servo/servo/forks",
177 | "mirror_url": null,
178 | "subscription_url": "https://api.github.com/repos/servo/servo/subscription",
179 | "merges_url": "https://api.github.com/repos/servo/servo/merges",
180 | "collaborators_url": "https://api.github.com/repos/servo/servo/collaborators{/collaborator}",
181 | "updated_at": "2015-08-07T09:37:49Z",
182 | "private": false,
183 | "pulls_url": "https://api.github.com/repos/servo/servo/pulls{/number}",
184 | "issue_comment_url": "https://api.github.com/repos/servo/servo/issues/comments{/number}",
185 | "full_name": "servo/servo",
186 | "owner": {
187 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
188 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
189 | "organizations_url": "https://api.github.com/users/servo/orgs",
190 | "url": "https://api.github.com/users/servo",
191 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
192 | "html_url": "https://github.com/servo",
193 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
194 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
195 | "repos_url": "https://api.github.com/users/servo/repos",
196 | "received_events_url": "https://api.github.com/users/servo/received_events",
197 | "gravatar_id": "",
198 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
199 | "site_admin": false,
200 | "login": "servo",
201 | "type": "Organization",
202 | "id": 2566135,
203 | "followers_url": "https://api.github.com/users/servo/followers"
204 | },
205 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/{sha}",
206 | "id": 3390243,
207 | "keys_url": "https://api.github.com/repos/servo/servo/keys{/key_id}",
208 | "size": 1903023,
209 | "tags_url": "https://api.github.com/repos/servo/servo/tags",
210 | "issue_events_url": "https://api.github.com/repos/servo/servo/issues/events{/number}",
211 | "contributors_url": "https://api.github.com/repos/servo/servo/contributors",
212 | "downloads_url": "https://api.github.com/repos/servo/servo/downloads",
213 | "has_downloads": true,
214 | "assignees_url": "https://api.github.com/repos/servo/servo/assignees{/user}",
215 | "contents_url": "https://api.github.com/repos/servo/servo/contents/{+path}",
216 | "has_pages": false,
217 | "git_refs_url": "https://api.github.com/repos/servo/servo/git/refs{/sha}",
218 | "clone_url": "https://github.com/servo/servo.git",
219 | "watchers_count": 4571,
220 | "git_tags_url": "https://api.github.com/repos/servo/servo/git/tags{/sha}",
221 | "milestones_url": "https://api.github.com/repos/servo/servo/milestones{/number}",
222 | "stargazers_count": 4571,
223 | "homepage": "",
224 | "branches_url": "https://api.github.com/repos/servo/servo/branches{/branch}",
225 | "fork": false,
226 | "commits_url": "https://api.github.com/repos/servo/servo/commits{/sha}",
227 | "releases_url": "https://api.github.com/repos/servo/servo/releases{/id}",
228 | "description": "The Servo Browser Engine",
229 | "archive_url": "https://api.github.com/repos/servo/servo/{archive_format}{/ref}",
230 | "labels_url": "https://api.github.com/repos/servo/servo/labels{/name}",
231 | "events_url": "https://api.github.com/repos/servo/servo/events",
232 | "comments_url": "https://api.github.com/repos/servo/servo/comments{/number}",
233 | "html_url": "https://github.com/servo/servo",
234 | "forks": 717,
235 | "compare_url": "https://api.github.com/repos/servo/servo/compare/{base}...{head}",
236 | "trees_url": "https://api.github.com/repos/servo/servo/git/trees{/sha}",
237 | "git_url": "git://github.com/servo/servo.git",
238 | "svn_url": "https://github.com/servo/servo",
239 | "notifications_url": "https://api.github.com/repos/servo/servo/notifications{?since,all,participating}",
240 | "has_issues": true,
241 | "ssh_url": "git@github.com:servo/servo.git",
242 | "blobs_url": "https://api.github.com/repos/servo/servo/git/blobs{/sha}",
243 | "languages_url": "https://api.github.com/repos/servo/servo/languages",
244 | "hooks_url": "https://api.github.com/repos/servo/servo/hooks",
245 | "open_issues_count": 1057,
246 | "watchers": 4571,
247 | "name": "servo",
248 | "language": "Rust",
249 | "url": "https://api.github.com/repos/servo/servo",
250 | "created_at": "2012-02-08T19:07:25Z",
251 | "pushed_at": "2015-08-07T16:09:10Z",
252 | "forks_count": 717,
253 | "default_branch": "master",
254 | "teams_url": "https://api.github.com/repos/servo/servo/teams",
255 | "open_issues": 1057,
256 | "git_commits_url": "https://api.github.com/repos/servo/servo/git/commits{/sha}",
257 | "subscribers_url": "https://api.github.com/repos/servo/servo/subscribers",
258 | "stargazers_url": "https://api.github.com/repos/servo/servo/stargazers"
259 | },
260 | "sha": "568bd92236e6eee0a3dfc077dccf74e4fdc79582",
261 | "ref": "master",
262 | "user": {
263 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
264 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
265 | "organizations_url": "https://api.github.com/users/servo/orgs",
266 | "url": "https://api.github.com/users/servo",
267 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
268 | "html_url": "https://github.com/servo",
269 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
270 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
271 | "repos_url": "https://api.github.com/users/servo/repos",
272 | "received_events_url": "https://api.github.com/users/servo/received_events",
273 | "gravatar_id": "",
274 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
275 | "site_admin": false,
276 | "login": "servo",
277 | "type": "Organization",
278 | "id": 2566135,
279 | "followers_url": "https://api.github.com/users/servo/followers"
280 | },
281 | "label": "servo:master"
282 | },
283 | "user": {
284 | "following_url": "https://api.github.com/users/Ms2ger/following{/other_user}",
285 | "gists_url": "https://api.github.com/users/Ms2ger/gists{/gist_id}",
286 | "organizations_url": "https://api.github.com/users/Ms2ger/orgs",
287 | "url": "https://api.github.com/users/Ms2ger",
288 | "events_url": "https://api.github.com/users/Ms2ger/events{/privacy}",
289 | "html_url": "https://github.com/Ms2ger",
290 | "subscriptions_url": "https://api.github.com/users/Ms2ger/subscriptions",
291 | "avatar_url": "https://avatars.githubusercontent.com/u/111161?v=3",
292 | "repos_url": "https://api.github.com/users/Ms2ger/repos",
293 | "received_events_url": "https://api.github.com/users/Ms2ger/received_events",
294 | "gravatar_id": "",
295 | "starred_url": "https://api.github.com/users/Ms2ger/starred{/owner}{/repo}",
296 | "site_admin": false,
297 | "login": "Ms2ger",
298 | "type": "User",
299 | "id": 111161,
300 | "followers_url": "https://api.github.com/users/Ms2ger/followers"
301 | },
302 | "milestone": null,
303 | "locked": false,
304 | "merged_by": null,
305 | "url": "https://api.github.com/repos/servo/servo/pulls/7062",
306 | "mergeable_state": "stable",
307 | "created_at": "2015-08-07T07:37:38Z",
308 | "review_comments_url": "https://api.github.com/repos/servo/servo/pulls/7062/comments",
309 | "review_comments": 0,
310 | "review_comment_url": "https://api.github.com/repos/servo/servo/pulls/comments{/number}",
311 | "merged": false
312 | }
313 | },
314 | "payload": {
315 | "number": 7062,
316 | "pull_request": {
317 | "draft": false,
318 | "mergeable": null,
319 | "base": {
320 | "repo": {
321 | "owner": {
322 | "login": "servo"
323 | },
324 | "name": "servo"
325 | }
326 | }
327 | },
328 | "action": "synchronize"
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/synchronize_not_mergeable.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review",
5 | "S-needs-rebase"
6 | ]
7 | },
8 | "initial": {
9 | "labels": [
10 | "S-needs-rebase"
11 | ],
12 | "pull_request": {
13 | "body": "This excludes:\r\n\r\n* tenacious, as it has not yet been updated to the current rustc\r\n (Manishearth/rust-tenacious#6);\r\n* ipc-channel, as it doesn't build on linux upstream.\n\n\n[
](https://reviewable.io/reviews/servo/servo/7062)\n\n",
14 | "merge_commit_sha": "30f3785911255fde11ab91ef5c0a9fe741174aff",
15 | "number": 7062,
16 | "assignee": null,
17 | "mergeable": false,
18 | "closed_at": null,
19 | "additions": 483,
20 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/289b421490113278d323ef482d77a1cacb560ac7",
21 | "id": 41866663,
22 | "title": "Update most dependencies.",
23 | "comments": 3,
24 | "merged_at": null,
25 | "state": "open",
26 | "_links": {
27 | "review_comment": {
28 | "href": "https://api.github.com/repos/servo/servo/pulls/comments{/number}"
29 | },
30 | "commits": {
31 | "href": "https://api.github.com/repos/servo/servo/pulls/7062/commits"
32 | },
33 | "self": {
34 | "href": "https://api.github.com/repos/servo/servo/pulls/7062"
35 | },
36 | "comments": {
37 | "href": "https://api.github.com/repos/servo/servo/issues/7062/comments"
38 | },
39 | "html": {
40 | "href": "https://github.com/servo/servo/pull/7062"
41 | },
42 | "review_comments": {
43 | "href": "https://api.github.com/repos/servo/servo/pulls/7062/comments"
44 | },
45 | "issue": {
46 | "href": "https://api.github.com/repos/servo/servo/issues/7062"
47 | },
48 | "statuses": {
49 | "href": "https://api.github.com/repos/servo/servo/statuses/289b421490113278d323ef482d77a1cacb560ac7"
50 | }
51 | },
52 | "commits": 1,
53 | "diff_url": "https://github.com/servo/servo/pull/7062.diff",
54 | "issue_url": "https://api.github.com/repos/servo/servo/issues/7062",
55 | "patch_url": "https://github.com/servo/servo/pull/7062.patch",
56 | "deletions": 438,
57 | "head": {
58 | "repo": {
59 | "issues_url": "https://api.github.com/repos/servo/servo/issues{/number}",
60 | "has_wiki": true,
61 | "forks_url": "https://api.github.com/repos/servo/servo/forks",
62 | "mirror_url": null,
63 | "subscription_url": "https://api.github.com/repos/servo/servo/subscription",
64 | "merges_url": "https://api.github.com/repos/servo/servo/merges",
65 | "collaborators_url": "https://api.github.com/repos/servo/servo/collaborators{/collaborator}",
66 | "updated_at": "2015-08-07T09:37:49Z",
67 | "private": false,
68 | "pulls_url": "https://api.github.com/repos/servo/servo/pulls{/number}",
69 | "issue_comment_url": "https://api.github.com/repos/servo/servo/issues/comments{/number}",
70 | "full_name": "servo/servo",
71 | "owner": {
72 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
73 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
74 | "organizations_url": "https://api.github.com/users/servo/orgs",
75 | "url": "https://api.github.com/users/servo",
76 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
77 | "html_url": "https://github.com/servo",
78 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
79 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
80 | "repos_url": "https://api.github.com/users/servo/repos",
81 | "received_events_url": "https://api.github.com/users/servo/received_events",
82 | "gravatar_id": "",
83 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
84 | "site_admin": false,
85 | "login": "servo",
86 | "type": "Organization",
87 | "id": 2566135,
88 | "followers_url": "https://api.github.com/users/servo/followers"
89 | },
90 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/{sha}",
91 | "id": 3390243,
92 | "keys_url": "https://api.github.com/repos/servo/servo/keys{/key_id}",
93 | "size": 1903023,
94 | "tags_url": "https://api.github.com/repos/servo/servo/tags",
95 | "issue_events_url": "https://api.github.com/repos/servo/servo/issues/events{/number}",
96 | "contributors_url": "https://api.github.com/repos/servo/servo/contributors",
97 | "downloads_url": "https://api.github.com/repos/servo/servo/downloads",
98 | "has_downloads": true,
99 | "assignees_url": "https://api.github.com/repos/servo/servo/assignees{/user}",
100 | "contents_url": "https://api.github.com/repos/servo/servo/contents/{+path}",
101 | "has_pages": false,
102 | "git_refs_url": "https://api.github.com/repos/servo/servo/git/refs{/sha}",
103 | "clone_url": "https://github.com/servo/servo.git",
104 | "watchers_count": 4571,
105 | "git_tags_url": "https://api.github.com/repos/servo/servo/git/tags{/sha}",
106 | "milestones_url": "https://api.github.com/repos/servo/servo/milestones{/number}",
107 | "stargazers_count": 4571,
108 | "homepage": "",
109 | "branches_url": "https://api.github.com/repos/servo/servo/branches{/branch}",
110 | "fork": false,
111 | "commits_url": "https://api.github.com/repos/servo/servo/commits{/sha}",
112 | "releases_url": "https://api.github.com/repos/servo/servo/releases{/id}",
113 | "description": "The Servo Browser Engine",
114 | "archive_url": "https://api.github.com/repos/servo/servo/{archive_format}{/ref}",
115 | "labels_url": "https://api.github.com/repos/servo/servo/labels{/name}",
116 | "events_url": "https://api.github.com/repos/servo/servo/events",
117 | "comments_url": "https://api.github.com/repos/servo/servo/comments{/number}",
118 | "html_url": "https://github.com/servo/servo",
119 | "forks": 717,
120 | "compare_url": "https://api.github.com/repos/servo/servo/compare/{base}...{head}",
121 | "trees_url": "https://api.github.com/repos/servo/servo/git/trees{/sha}",
122 | "git_url": "git://github.com/servo/servo.git",
123 | "svn_url": "https://github.com/servo/servo",
124 | "notifications_url": "https://api.github.com/repos/servo/servo/notifications{?since,all,participating}",
125 | "has_issues": true,
126 | "ssh_url": "git@github.com:servo/servo.git",
127 | "blobs_url": "https://api.github.com/repos/servo/servo/git/blobs{/sha}",
128 | "languages_url": "https://api.github.com/repos/servo/servo/languages",
129 | "hooks_url": "https://api.github.com/repos/servo/servo/hooks",
130 | "open_issues_count": 1057,
131 | "watchers": 4571,
132 | "name": "servo",
133 | "language": "Rust",
134 | "url": "https://api.github.com/repos/servo/servo",
135 | "created_at": "2012-02-08T19:07:25Z",
136 | "pushed_at": "2015-08-07T16:09:10Z",
137 | "forks_count": 717,
138 | "default_branch": "master",
139 | "teams_url": "https://api.github.com/repos/servo/servo/teams",
140 | "open_issues": 1057,
141 | "git_commits_url": "https://api.github.com/repos/servo/servo/git/commits{/sha}",
142 | "subscribers_url": "https://api.github.com/repos/servo/servo/subscribers",
143 | "stargazers_url": "https://api.github.com/repos/servo/servo/stargazers"
144 | },
145 | "sha": "289b421490113278d323ef482d77a1cacb560ac7",
146 | "ref": "update-some",
147 | "user": {
148 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
149 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
150 | "organizations_url": "https://api.github.com/users/servo/orgs",
151 | "url": "https://api.github.com/users/servo",
152 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
153 | "html_url": "https://github.com/servo",
154 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
155 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
156 | "repos_url": "https://api.github.com/users/servo/repos",
157 | "received_events_url": "https://api.github.com/users/servo/received_events",
158 | "gravatar_id": "",
159 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
160 | "site_admin": false,
161 | "login": "servo",
162 | "type": "Organization",
163 | "id": 2566135,
164 | "followers_url": "https://api.github.com/users/servo/followers"
165 | },
166 | "label": "servo:update-some"
167 | },
168 | "commits_url": "https://api.github.com/repos/servo/servo/pulls/7062/commits",
169 | "changed_files": 3,
170 | "comments_url": "https://api.github.com/repos/servo/servo/issues/7062/comments",
171 | "html_url": "https://github.com/servo/servo/pull/7062",
172 | "updated_at": "2015-08-07T16:09:10Z",
173 | "base": {
174 | "repo": {
175 | "issues_url": "https://api.github.com/repos/servo/servo/issues{/number}",
176 | "has_wiki": true,
177 | "forks_url": "https://api.github.com/repos/servo/servo/forks",
178 | "mirror_url": null,
179 | "subscription_url": "https://api.github.com/repos/servo/servo/subscription",
180 | "merges_url": "https://api.github.com/repos/servo/servo/merges",
181 | "collaborators_url": "https://api.github.com/repos/servo/servo/collaborators{/collaborator}",
182 | "updated_at": "2015-08-07T09:37:49Z",
183 | "private": false,
184 | "pulls_url": "https://api.github.com/repos/servo/servo/pulls{/number}",
185 | "issue_comment_url": "https://api.github.com/repos/servo/servo/issues/comments{/number}",
186 | "full_name": "servo/servo",
187 | "owner": {
188 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
189 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
190 | "organizations_url": "https://api.github.com/users/servo/orgs",
191 | "url": "https://api.github.com/users/servo",
192 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
193 | "html_url": "https://github.com/servo",
194 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
195 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
196 | "repos_url": "https://api.github.com/users/servo/repos",
197 | "received_events_url": "https://api.github.com/users/servo/received_events",
198 | "gravatar_id": "",
199 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
200 | "site_admin": false,
201 | "login": "servo",
202 | "type": "Organization",
203 | "id": 2566135,
204 | "followers_url": "https://api.github.com/users/servo/followers"
205 | },
206 | "statuses_url": "https://api.github.com/repos/servo/servo/statuses/{sha}",
207 | "id": 3390243,
208 | "keys_url": "https://api.github.com/repos/servo/servo/keys{/key_id}",
209 | "size": 1903023,
210 | "tags_url": "https://api.github.com/repos/servo/servo/tags",
211 | "issue_events_url": "https://api.github.com/repos/servo/servo/issues/events{/number}",
212 | "contributors_url": "https://api.github.com/repos/servo/servo/contributors",
213 | "downloads_url": "https://api.github.com/repos/servo/servo/downloads",
214 | "has_downloads": true,
215 | "assignees_url": "https://api.github.com/repos/servo/servo/assignees{/user}",
216 | "contents_url": "https://api.github.com/repos/servo/servo/contents/{+path}",
217 | "has_pages": false,
218 | "git_refs_url": "https://api.github.com/repos/servo/servo/git/refs{/sha}",
219 | "clone_url": "https://github.com/servo/servo.git",
220 | "watchers_count": 4571,
221 | "git_tags_url": "https://api.github.com/repos/servo/servo/git/tags{/sha}",
222 | "milestones_url": "https://api.github.com/repos/servo/servo/milestones{/number}",
223 | "stargazers_count": 4571,
224 | "homepage": "",
225 | "branches_url": "https://api.github.com/repos/servo/servo/branches{/branch}",
226 | "fork": false,
227 | "commits_url": "https://api.github.com/repos/servo/servo/commits{/sha}",
228 | "releases_url": "https://api.github.com/repos/servo/servo/releases{/id}",
229 | "description": "The Servo Browser Engine",
230 | "archive_url": "https://api.github.com/repos/servo/servo/{archive_format}{/ref}",
231 | "labels_url": "https://api.github.com/repos/servo/servo/labels{/name}",
232 | "events_url": "https://api.github.com/repos/servo/servo/events",
233 | "comments_url": "https://api.github.com/repos/servo/servo/comments{/number}",
234 | "html_url": "https://github.com/servo/servo",
235 | "forks": 717,
236 | "compare_url": "https://api.github.com/repos/servo/servo/compare/{base}...{head}",
237 | "trees_url": "https://api.github.com/repos/servo/servo/git/trees{/sha}",
238 | "git_url": "git://github.com/servo/servo.git",
239 | "svn_url": "https://github.com/servo/servo",
240 | "notifications_url": "https://api.github.com/repos/servo/servo/notifications{?since,all,participating}",
241 | "has_issues": true,
242 | "ssh_url": "git@github.com:servo/servo.git",
243 | "blobs_url": "https://api.github.com/repos/servo/servo/git/blobs{/sha}",
244 | "languages_url": "https://api.github.com/repos/servo/servo/languages",
245 | "hooks_url": "https://api.github.com/repos/servo/servo/hooks",
246 | "open_issues_count": 1057,
247 | "watchers": 4571,
248 | "name": "servo",
249 | "language": "Rust",
250 | "url": "https://api.github.com/repos/servo/servo",
251 | "created_at": "2012-02-08T19:07:25Z",
252 | "pushed_at": "2015-08-07T16:09:10Z",
253 | "forks_count": 717,
254 | "default_branch": "master",
255 | "teams_url": "https://api.github.com/repos/servo/servo/teams",
256 | "open_issues": 1057,
257 | "git_commits_url": "https://api.github.com/repos/servo/servo/git/commits{/sha}",
258 | "subscribers_url": "https://api.github.com/repos/servo/servo/subscribers",
259 | "stargazers_url": "https://api.github.com/repos/servo/servo/stargazers"
260 | },
261 | "sha": "568bd92236e6eee0a3dfc077dccf74e4fdc79582",
262 | "ref": "master",
263 | "user": {
264 | "following_url": "https://api.github.com/users/servo/following{/other_user}",
265 | "gists_url": "https://api.github.com/users/servo/gists{/gist_id}",
266 | "organizations_url": "https://api.github.com/users/servo/orgs",
267 | "url": "https://api.github.com/users/servo",
268 | "events_url": "https://api.github.com/users/servo/events{/privacy}",
269 | "html_url": "https://github.com/servo",
270 | "subscriptions_url": "https://api.github.com/users/servo/subscriptions",
271 | "avatar_url": "https://avatars.githubusercontent.com/u/2566135?v=3",
272 | "repos_url": "https://api.github.com/users/servo/repos",
273 | "received_events_url": "https://api.github.com/users/servo/received_events",
274 | "gravatar_id": "",
275 | "starred_url": "https://api.github.com/users/servo/starred{/owner}{/repo}",
276 | "site_admin": false,
277 | "login": "servo",
278 | "type": "Organization",
279 | "id": 2566135,
280 | "followers_url": "https://api.github.com/users/servo/followers"
281 | },
282 | "label": "servo:master"
283 | },
284 | "user": {
285 | "following_url": "https://api.github.com/users/Ms2ger/following{/other_user}",
286 | "gists_url": "https://api.github.com/users/Ms2ger/gists{/gist_id}",
287 | "organizations_url": "https://api.github.com/users/Ms2ger/orgs",
288 | "url": "https://api.github.com/users/Ms2ger",
289 | "events_url": "https://api.github.com/users/Ms2ger/events{/privacy}",
290 | "html_url": "https://github.com/Ms2ger",
291 | "subscriptions_url": "https://api.github.com/users/Ms2ger/subscriptions",
292 | "avatar_url": "https://avatars.githubusercontent.com/u/111161?v=3",
293 | "repos_url": "https://api.github.com/users/Ms2ger/repos",
294 | "received_events_url": "https://api.github.com/users/Ms2ger/received_events",
295 | "gravatar_id": "",
296 | "starred_url": "https://api.github.com/users/Ms2ger/starred{/owner}{/repo}",
297 | "site_admin": false,
298 | "login": "Ms2ger",
299 | "type": "User",
300 | "id": 111161,
301 | "followers_url": "https://api.github.com/users/Ms2ger/followers"
302 | },
303 | "milestone": null,
304 | "locked": false,
305 | "merged_by": null,
306 | "url": "https://api.github.com/repos/servo/servo/pulls/7062",
307 | "mergeable_state": "stable",
308 | "created_at": "2015-08-07T07:37:38Z",
309 | "review_comments_url": "https://api.github.com/repos/servo/servo/pulls/7062/comments",
310 | "review_comments": 0,
311 | "review_comment_url": "https://api.github.com/repos/servo/servo/pulls/comments{/number}",
312 | "merged": false
313 | }
314 | },
315 | "payload": {
316 | "number": 7062,
317 | "pull_request": {
318 | "draft": false,
319 | "mergeable": null,
320 | "base": {
321 | "repo": {
322 | "owner": {
323 | "login": "servo"
324 | },
325 | "name": "servo"
326 | }
327 | }
328 | },
329 | "action": "synchronize"
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/handlers/status_update/tests/unmergeable_pr_ready_for_review.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "labels": [
4 | "S-awaiting-review",
5 | "S-needs-rebase"
6 | ]
7 | },
8 | "initial": {},
9 | "payload": {
10 | "number": 7076,
11 | "pull_request": {
12 | "draft": false,
13 | "mergeable": false,
14 | "base": {
15 | "repo": {
16 | "owner": {
17 | "login": "servo"
18 | },
19 | "name": "servo"
20 | }
21 | }
22 | },
23 | "action": "ready_for_review"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/handlers/unsafe/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from eventhandler import EventHandler
4 |
5 |
6 | unsafe_warning_msg = ('These commits modify **unsafe code**. '
7 | 'Please review it carefully!')
8 |
9 |
10 | class UnsafeHandler(EventHandler):
11 | def on_pr_opened(self, api, payload):
12 | return
13 | for line in api.get_added_lines():
14 | if line.find('unsafe ') > -1:
15 | self.warn(unsafe_warning_msg)
16 | return
17 |
18 |
19 | handler_interface = UnsafeHandler
20 |
--------------------------------------------------------------------------------
/handlers/unsafe/tests/new_pr.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": {
3 | "comments": 1
4 | },
5 | "initial": {
6 | "diff": "+ unsafe fn foo()"
7 | },
8 | "payload": {
9 | "number": 7076,
10 | "pull_request": {
11 | "draft": false,
12 | "base": {
13 | "repo": {
14 | "owner": {
15 | "login": "servo"
16 | },
17 | "name": "servo"
18 | }
19 | }
20 | },
21 | "action": "opened"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/handlers/watchers/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from collections import defaultdict
4 | import os
5 | import fnmatch
6 |
7 | from eventhandler import EventHandler
8 | from helpers import get_people_from_config
9 |
10 | WATCHERS_CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'watchers.ini')
11 |
12 |
13 | def build_message(mentions):
14 | message = ['Heads up! This PR modifies the following files:']
15 | for (watcher, file_names) in mentions.items():
16 | message.append(" * @{}: {}".format(watcher, ', '.join(file_names[:5])))
17 | remaining = len(file_names[5:])
18 | if remaining:
19 | message[-1] += " and {} more".format(remaining)
20 |
21 | return '\n'.join(message)
22 |
23 |
24 | class WatchersHandler(EventHandler):
25 | def on_pr_opened(self, api, payload):
26 | if "pull_request" not in payload:
27 | return
28 |
29 | user = payload['pull_request']['user']['login']
30 |
31 | watchers = get_people_from_config(api, WATCHERS_CONFIG_FILE)
32 | if not watchers:
33 | return
34 |
35 | mentions = defaultdict(list)
36 |
37 | for (watcher, watched_files) in watchers:
38 | watched_files = watched_files.split(' ')
39 | blacklisted_files = []
40 |
41 | for watched_file in watched_files:
42 | if watched_file.startswith('-'):
43 | blacklisted_files.append(watched_file[1:])
44 | for blacklisted_file in blacklisted_files:
45 | watched_files.remove('-' + blacklisted_file)
46 |
47 | for filepath in api.get_changed_files():
48 | comment = False
49 | for watched_file in watched_files:
50 | if fnmatch.fnmatch(filepath, watched_file):
51 | comment = True
52 | for blacklisted_file in blacklisted_files:
53 | if (fnmatch.fnmatch(filepath, blacklisted_file)):
54 | comment = False
55 | if (comment and user != watcher):
56 | mentions[watcher].append(filepath)
57 | if not mentions:
58 | return
59 |
60 | message = build_message(mentions)
61 | api.post_comment(message)
62 |
63 |
64 | handler_interface = WatchersHandler
65 |
--------------------------------------------------------------------------------
/handlers/watchers/tests/new_pr_author_is_not_watcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 1
5 | },
6 | {
7 | "comments": 1
8 | },
9 | {
10 | "comments": 0
11 | },
12 | {
13 | "comments": 1
14 | },
15 | {
16 | "comments": 0
17 | },
18 | {
19 | "comments": 1
20 | },
21 | {
22 | "comments": 1
23 | }
24 | ],
25 | "initial": [
26 | {
27 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/file.rs"
28 | },
29 | {
30 | "diff": "diff --git a/watched/dir/file.rs /dev/null"
31 | },
32 | {
33 | "diff": "diff --git a/not-watched/dir/not-watched-file.rs b/not-watched/dir/not-watched-file.rs"
34 | },
35 | {
36 | "diff": "diff --git a/not-watched/dir/not-watched-file.rs b/not-watched/dir/watched-file.rs"
37 | },
38 | {
39 | "diff": "diff --git a/blacklisted/dir/watched-file.rs b/blacklisted/dir/watched-file.rs"
40 | },
41 | {
42 | "diff": "diff --git a/watched/dir/blacklisted-file.rs b/watched/dir/file.rs"
43 | },
44 | {
45 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/blacklisted-file.rs"
46 | }
47 | ],
48 | "payload": {
49 | "number": 7076,
50 | "pull_request": {
51 | "draft": false,
52 | "base": {
53 | "repo": {
54 | "owner": {
55 | "login": "servo"
56 | },
57 | "name": "highfive-test"
58 | }
59 | },
60 | "user": {
61 | "login": "not_a_watcher"
62 | }
63 | },
64 | "action": "opened"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/handlers/watchers/tests/new_pr_author_is_watcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 0
5 | },
6 | {
7 | "comments": 0
8 | },
9 | {
10 | "comments": 0
11 | },
12 | {
13 | "comments": 0
14 | },
15 | {
16 | "comments": 0
17 | },
18 | {
19 | "comments": 0
20 | },
21 | {
22 | "comments": 0
23 | }
24 | ],
25 | "initial": [
26 | {
27 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/file.rs"
28 | },
29 | {
30 | "diff": "diff --git a/watched/dir/file.rs /dev/null"
31 | },
32 | {
33 | "diff": "diff --git a/not-watched/dir/not-watched-file.rs b/not-watched/dir/not-watched-file.rs"
34 | },
35 | {
36 | "diff": "diff --git a/not-watched/dir/not-watched-file.rs b/not-watched/dir/watched-file.rs"
37 | },
38 | {
39 | "diff": "diff --git a/blacklisted/dir/watched-file.rs b/blacklisted/dir/watched-file.rs"
40 | },
41 | {
42 | "diff": "diff --git a/watched/dir/blacklisted-file.rs b/watched/dir/file.rs"
43 | },
44 | {
45 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/blacklisted-file.rs"
46 | }
47 | ],
48 | "payload": {
49 | "number": 7076,
50 | "pull_request": {
51 | "draft": false,
52 | "base": {
53 | "repo": {
54 | "owner": {
55 | "login": "servo"
56 | },
57 | "name": "highfive-test"
58 | }
59 | },
60 | "user": {
61 | "login": "test_user"
62 | }
63 | },
64 | "action": "opened"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/handlers/watchers/tests/new_pr_wildcards_author_is_not_watcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 1
5 | },
6 | {
7 | "comments": 1
8 | },
9 | {
10 | "comments": 0
11 | },
12 | {
13 | "comments": 0
14 | },
15 | {
16 | "comments": 0
17 | },
18 | {
19 | "comments": 1
20 | },
21 | {
22 | "comments": 0
23 | },
24 | {
25 | "comments": 0
26 | }
27 | ],
28 | "initial": [
29 | {
30 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/file.rs"
31 | },
32 | {
33 | "diff": "diff --git a/not-watched/dir/watched-file.rs b/not-watched/dir/watched-file.rs"
34 | },
35 | {
36 | "diff": "diff --git a/blacklisted/dir/watched-file.rs b/blacklisted/dir/watched-file.rs"
37 | },
38 | {
39 | "diff": "diff --git a/blacklisted/dir/file.rs b/blacklisted/dir/file.py"
40 | },
41 | {
42 | "diff": "diff --git a/watched/dir/blacklisted-file.rs b/watched/dir/blacklisted-file.rs"
43 | },
44 | {
45 | "diff": "diff --git a/not-watched/dir/file.txt b/not-watched/dir/file.txt"
46 | },
47 | {
48 | "diff": "diff --git a/not-watched/dir/file.rs b/not-watched/dir/file.rs"
49 | },
50 | {
51 | "diff": "diff --git a/not-watched/dir/black-listed-file.txt b/not-watched/dir/black-listed-file.txt"
52 | }
53 | ],
54 | "payload": {
55 | "number": 7076,
56 | "pull_request": {
57 | "draft": false,
58 | "base": {
59 | "repo": {
60 | "owner": {
61 | "login": "servo"
62 | },
63 | "name": "highfive-test"
64 | }
65 | },
66 | "user": {
67 | "login": "not_test_user"
68 | }
69 | },
70 | "action": "opened"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/handlers/watchers/tests/new_pr_wildcards_author_is_watcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "expected": [
3 | {
4 | "comments": 0
5 | },
6 | {
7 | "comments": 0
8 | },
9 | {
10 | "comments": 0
11 | },
12 | {
13 | "comments": 0
14 | },
15 | {
16 | "comments": 0
17 | },
18 | {
19 | "comments": 0
20 | },
21 | {
22 | "comments": 0
23 | },
24 | {
25 | "comments": 0
26 | }
27 | ],
28 | "initial": [
29 | {
30 | "diff": "diff --git a/watched/dir/file.rs b/watched/dir/file.rs"
31 | },
32 | {
33 | "diff": "diff --git a/not-watched/dir/watched-file.rs b/not-watched/dir/watched-file.rs"
34 | },
35 | {
36 | "diff": "diff --git a/blacklisted/dir/watched-file.rs b/blacklisted/dir/watched-file.rs"
37 | },
38 | {
39 | "diff": "diff --git a/blacklisted/dir/file.rs b/blacklisted/dir/file.py"
40 | },
41 | {
42 | "diff": "diff --git a/watched/dir/blacklisted-file.rs b/watched/dir/blacklisted-file.rs"
43 | },
44 | {
45 | "diff": "diff --git a/not-watched/dir/file.txt b/not-watched/dir/file.txt"
46 | },
47 | {
48 | "diff": "diff --git a/not-watched/dir/file.rs b/not-watched/dir/file.rs"
49 | },
50 | {
51 | "diff": "diff --git a/not-watched/dir/black-listed-file.txt b/not-watched/dir/black-listed-file.txt"
52 | }
53 | ],
54 | "payload": {
55 | "number": 7076,
56 | "pull_request": {
57 | "draft": false,
58 | "base": {
59 | "repo": {
60 | "owner": {
61 | "login": "servo"
62 | },
63 | "name": "highfive-test"
64 | }
65 | },
66 | "user": {
67 | "login": "test_user"
68 | }
69 | },
70 | "action": "opened"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/handlers/watchers/watchers.ini:
--------------------------------------------------------------------------------
1 | [servo/servo]
2 | emilio =
3 |
4 | [servo/highfive]
5 | jdm =
6 |
7 | [servo/highfive-test]
8 | test_user = watched/dir/* not-watched/dir/watched-file.rs blacklisted/dir/watched-file.rs -blacklisted/dir/* -watched/dir/blacklisted-file.rs not-watched/dir/*.txt -not-watched/dir/black-listed-file.txt
9 |
--------------------------------------------------------------------------------
/helpers.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from configparser import ConfigParser
4 | import os
5 |
6 | COLLABORATORS_CONFIG_FILE = os.path.join(os.path.dirname(__file__),
7 | 'collaborators.ini')
8 |
9 |
10 | _test_path_roots = ['a/', 'b/']
11 |
12 |
13 | def get_people_from_config(api, config_abs_path):
14 | '''
15 | Gets the people listed under a particular repo from a config file.
16 | Note that the names (despite how they're in the file) will always
17 | be parsed to 'lowercase'.
18 | '''
19 | config = ConfigParser()
20 | config.read(config_abs_path)
21 | repo = api.owner + '/' + api.repo
22 |
23 | try:
24 | return config.items(repo)
25 | except ConfigParser.NoSectionError:
26 | return [] # No people
27 |
28 |
29 | def get_collaborators(api):
30 | config_items = get_people_from_config(api, COLLABORATORS_CONFIG_FILE)
31 | return [username for (username, _) in config_items]
32 |
33 |
34 | def is_addition(diff_line):
35 | """
36 | Checks if a line from a unified diff is an addition.
37 | """
38 | return diff_line.startswith('+') and not diff_line.startswith('+++')
39 |
40 |
41 | def normalize_file_path(filepath):
42 | """
43 | Strip any leading/training whitespace.
44 | Remove any test directories from the start of the path
45 | """
46 | if filepath is None or filepath.strip() == '':
47 | return None
48 | filepath = filepath.strip()
49 | for prefix in _test_path_roots:
50 | if filepath.startswith(prefix):
51 | return filepath[len(prefix):]
52 | return filepath
53 |
54 |
55 | def linear_search(sequence, element, callback=lambda thing: thing):
56 | """
57 | The 'in' operator also does a linear search over a sequence, but it checks
58 | for the exact match of the given object, whereas this makes use of '==',
59 | which calls the '__eq__' magic method, which could've been overridden in
60 | a custom class (which is the case for our test lint)
61 | """
62 | for thing in sequence:
63 | if element == thing: # element could have an overridden '__eq__'
64 | callback(thing)
65 |
--------------------------------------------------------------------------------
/json_cleanup.py:
--------------------------------------------------------------------------------
1 | NODE_SEP = ' -> '
2 |
3 |
4 | def visit_nodes(node): # simple recursive tree traversal
5 | if hasattr(node, 'mark'): # it's already a NodeMarker
6 | return node
7 | if hasattr(node, '__iter__') and not isinstance(node, str):
8 | iterator = range(len(node)) if isinstance(node, list) else node
9 | for thing in iterator:
10 | node[thing] = visit_nodes(node[thing])
11 | return NodeMarker(node)
12 |
13 |
14 | # We need the roots of each node, so that we can trace our way back to the
15 | # root from a specific node (marking nodes along the way).
16 | # Since `visit_nodes` makes a pre-order traversal, it assigns `NodeMarker`
17 | # to each node from inside-out, which makes it difficult to assign roots
18 | # So, we do another traversal to store the references of the root nodes
19 | def assign_roots(marker_node, root=None):
20 | node = marker_node._node
21 | if hasattr(node, '__iter__') and not isinstance(node, str):
22 | iterator = range(len(node)) if isinstance(node, list) else node
23 | for thing in iterator:
24 | assign_roots(node[thing], marker_node)
25 | marker_node._root = root
26 |
27 |
28 | class NodeMarker(object):
29 | def __init__(self, node, root=None):
30 | self._root = root
31 | self._node = node # actual value
32 | self._is_used = False # marker
33 |
34 | def mark(self):
35 | self._is_used = True
36 | root = self._root
37 | while root and not root._is_used:
38 | root._is_used = True
39 | root = root._root
40 |
41 | def get_object(self, obj):
42 | return obj._node if hasattr(obj, 'mark') else obj
43 |
44 | # The following methods blindly assume that the method is supported by the
45 | # particular type (i.e., exceptions should be handled explicitly)
46 |
47 | def lower(self):
48 | return str(self).lower()
49 |
50 | # if you access the element in the usual way, then "bam!"
51 | def __getitem__(self, key):
52 | self._node[key].mark() # it will be marked as used!
53 | return self._node[key]
54 |
55 | def get(self, key, default=None):
56 | if key in self._node:
57 | self._node[key].mark()
58 | return self._node.get(key, default)
59 |
60 | def __setitem__(self, key, val):
61 | self._node[key] = visit_nodes(val)
62 |
63 | def __hash__(self):
64 | return hash(self._node)
65 |
66 | def __iter__(self):
67 | return iter(self._node)
68 |
69 | def __eq__(self, other):
70 | return self._node == self.get_object(other)
71 |
72 | def __ne__(self, other):
73 | return self._node != self.get_object(other)
74 |
75 | def __add__(self, other):
76 | return self._node + self.get_object(other)
77 |
78 | def __mod__(self, other):
79 | return self._node % self.get_object(other)
80 |
81 | def __contains__(self, other):
82 | other = self.get_object(other)
83 | # since string is also a sequence in python, we shouldn't iterate
84 | # over it and check the individual characters
85 | if isinstance(self._node, str) or isinstance(self._node, bytes):
86 | return other in self._node
87 |
88 | for idx, thing in enumerate(self._node):
89 | if thing == other:
90 | if isinstance(self._node, list):
91 | self._node[idx].mark()
92 | else:
93 | self._node[thing].mark()
94 | return True
95 | return False
96 |
97 | def __str__(self):
98 | return str(self._node)
99 |
100 | def __int__(self):
101 | return int(self._node)
102 |
103 |
104 | class JsonCleaner(object):
105 | def __init__(self, json_obj):
106 | self.unused = 0
107 | self.json = visit_nodes(json_obj)
108 | assign_roots(self.json)
109 |
110 | def clean(self, warn=True):
111 | return self._filter_nodes(self.json, warn)
112 |
113 | def _filter_nodes(self, marker_node, warn, path=''):
114 | if marker_node._is_used:
115 | node = marker_node._node
116 | if hasattr(node, '__iter__') and not isinstance(node, str):
117 | # it's either 'list' or 'dict' when it comes to JSONs
118 | removed = 0
119 | iterator = range(len(node)) if isinstance(node, list) \
120 | else node.keys()
121 | for thing in iterator:
122 | new_path = path + str(thing) + NODE_SEP
123 | # since lists maintain order, once we pop them,
124 | # we decrement their indices as their length is reduced
125 | if isinstance(node, list):
126 | thing -= removed
127 | node[thing] = self._filter_nodes(node[thing],
128 | warn, new_path)
129 | if node[thing] == ():
130 | self.unused += 1
131 | if warn:
132 | new_path = new_path.strip(NODE_SEP)
133 | print('unused node at "%s"' % new_path)
134 | node.pop(thing)
135 | removed += 1
136 | return node
137 | return ()
138 |
--------------------------------------------------------------------------------
/newpr.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from __future__ import absolute_import, print_function
4 |
5 | from base64 import standard_b64encode
6 | import cgi
7 | from configparser import RawConfigParser
8 | import contextlib
9 | import gzip
10 | try:
11 | import simplejson as json
12 | except ImportError:
13 | import json
14 | from io import StringIO
15 | from urllib import request, error
16 |
17 | import eventhandler
18 | from helpers import is_addition, normalize_file_path
19 |
20 | DIFF_HEADER_PREFIX = 'diff --git '
21 |
22 |
23 | class APIProvider(object):
24 | def __init__(self, payload, user):
25 | (owner, repo, issue) = extract_globals_from_payload(payload)
26 | self.owner = owner
27 | self.repo = repo
28 | self.issue = issue
29 | self.user = user
30 | self.changed_files = None
31 |
32 | def is_new_contributor(self, username):
33 | raise NotImplementedError
34 |
35 | def post_comment(self, body):
36 | raise NotImplementedError
37 |
38 | def add_label(self, label):
39 | raise NotImplementedError
40 |
41 | def remove_label(self, label):
42 | raise NotImplementedError
43 |
44 | def get_labels(self):
45 | raise NotImplementedError
46 |
47 | def get_diff(self):
48 | return NotImplementedError
49 |
50 | def set_assignee(self, assignee):
51 | raise NotImplementedError
52 |
53 | def get_pull(self):
54 | raise NotImplementedError
55 |
56 | def get_page_content(self, url):
57 | raise NotImplementedError
58 |
59 | def get_diff_headers(self):
60 | diff = self.get_diff()
61 | for line in diff.splitlines():
62 | if line.startswith(DIFF_HEADER_PREFIX):
63 | yield line
64 |
65 | def get_changed_files(self):
66 | if self.changed_files is None:
67 | changed_files = []
68 | for line in self.get_diff_headers():
69 | files = line.split(DIFF_HEADER_PREFIX)[-1].split(' ')
70 | changed_files.extend(files)
71 |
72 | # And get unique values using `set()`
73 | normalized = map(normalize_file_path, changed_files)
74 | self.changed_files = set(f for f in normalized if f is not None)
75 | return self.changed_files
76 |
77 | def get_added_lines(self):
78 | diff = self.get_diff()
79 | for line in diff.splitlines():
80 | if is_addition(line):
81 | # prefix of one or two pluses (+)
82 | yield line
83 |
84 |
85 | class GithubAPIProvider(APIProvider):
86 | BASE_URL = "https://api.github.com/repos/"
87 | contributors_url = BASE_URL + "%s/%s/contributors?per_page=400"
88 | post_comment_url = BASE_URL + "%s/%s/issues/%s/comments"
89 | collaborators_url = BASE_URL + "%s/%s/collaborators"
90 | issue_url = BASE_URL + "%s/%s/issues/%s"
91 | get_label_url = BASE_URL + "%s/%s/issues/%s/labels"
92 | add_label_url = BASE_URL + "%s/%s/issues/%s/labels"
93 | remove_label_url = BASE_URL + "%s/%s/issues/%s/labels/%s"
94 |
95 | def __init__(self, payload, user, token):
96 | APIProvider.__init__(self, payload, user)
97 | self.token = token
98 | self._labels = None
99 | self._diff = None
100 | self.diff_url = None
101 | self.pull_url = None
102 | if "issue" in payload and "pull_request" in payload["issue"]:
103 | self.diff_url = payload["issue"]["pull_request"]["diff_url"]
104 | self.pull_url = payload["issue"]["pull_request"]["url"]
105 | if "pull_request" in payload:
106 | self.diff_url = payload["pull_request"]["diff_url"]
107 | self.pull_url = payload["pull_request"]["url"]
108 |
109 | def api_req(self, method, url, data=None, media_type=None):
110 | data = None if not data else json.dumps(data).encode('utf-8')
111 | headers = {} if not data else {'Content-Type': 'application/json'}
112 | req = request.Request(url, data, headers)
113 | req.get_method = lambda: method
114 | if self.token:
115 | authorization = '%s:%s' % (self.user, self.token)
116 | encoded = authorization.encode('utf-8')
117 | decoded = standard_b64encode(encoded).decode('utf-8')
118 | base64string = decoded.replace('\n', '')
119 | req.add_header("Authorization", "Basic %s" % base64string)
120 |
121 | if media_type:
122 | req.add_header("Accept", media_type)
123 | f = request.urlopen(req)
124 | header = f.info()
125 | if header.get('Content-Encoding') == 'gzip':
126 | buf = StringIO(f.read())
127 | f = gzip.GzipFile(fileobj=buf)
128 | return {"header": header, "body": f.read()}
129 |
130 | # This function is adapted from https://github.com/kennethreitz/requests/blob/209a871b638f85e2c61966f82e547377ed4260d9/requests/utils.py#L562 # noqa
131 | # Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
132 | def parse_header_links(self, value):
133 | if not value:
134 | return None
135 |
136 | links = {}
137 | replace_chars = " '\""
138 | for val in value.split(","):
139 | try:
140 | url, params = val.split(";", 1)
141 | except ValueError:
142 | url, params = val, ''
143 |
144 | url = url.strip("<> '\"")
145 |
146 | for param in params.split(";"):
147 | try:
148 | key, value = param.split("=")
149 | except ValueError:
150 | break
151 | key = key.strip(replace_chars)
152 | if key == 'rel':
153 | links[value.strip(replace_chars)] = url
154 |
155 | return links
156 |
157 | def is_new_contributor(self, username):
158 | url = self.contributors_url % (self.owner, self.repo)
159 | # iterate through the pages to try and find the contributor
160 | while True:
161 | stats_raw = self.api_req("GET", url)
162 | stats = json.loads(stats_raw['body'])
163 | links = self.parse_header_links(stats_raw['header'].get('Link'))
164 |
165 | for contributor in stats:
166 | if contributor['login'] == username:
167 | return False
168 |
169 | if not links or 'next' not in links:
170 | return True
171 | url = links['next']
172 |
173 | def post_comment(self, body):
174 | url = self.post_comment_url % (self.owner, self.repo, self.issue)
175 | try:
176 | self.api_req("POST", url, {"body": body})
177 | except error.HTTPError as e:
178 | if e.code == 201:
179 | pass
180 | else:
181 | raise e
182 |
183 | def add_label(self, label):
184 | url = self.add_label_url % (self.owner, self.repo, self.issue)
185 | if self._labels:
186 | self._labels += [label]
187 | try:
188 | self.api_req("POST", url, [label])
189 | except error.HTTPError as e:
190 | if e.code == 201:
191 | pass
192 | else:
193 | raise e
194 |
195 | def remove_label(self, label):
196 | url = self.remove_label_url % (self.owner, self.repo, self.issue,
197 | label)
198 | if self._labels and label in self._labels:
199 | self._labels.remove(label)
200 | try:
201 | self.api_req("DELETE", url, {})
202 | except error.HTTPError:
203 | pass
204 |
205 | def get_labels(self):
206 | url = self.get_label_url % (self.owner, self.repo, self.issue)
207 | if self._labels is not None:
208 | return self._labels
209 | try:
210 | result = self.api_req("GET", url)
211 | except error.HTTPError as e:
212 | if e.code == 201:
213 | pass
214 | else:
215 | raise e
216 | self._labels = list(map(
217 | lambda x: x["name"],
218 | json.loads(result['body'])
219 | ))
220 | return self._labels
221 |
222 | def get_diff(self):
223 | if not self.diff_url:
224 | return ''
225 | if self._diff:
226 | return self._diff
227 | self._diff = self.api_req("GET", self.diff_url)['body'].decode('utf-8')
228 | return self._diff
229 |
230 | def set_assignee(self, assignee):
231 | url = self.issue_url % (self.owner, self.repo, self.issue)
232 | try:
233 | self.api_req("PATCH", url, {"assignee": assignee})['body']
234 | except error.HTTPError as e:
235 | if e.code == 201:
236 | pass
237 | else:
238 | raise e
239 |
240 | def get_pull(self):
241 | return json.loads(self.api_req("GET", self.pull_url)["body"])
242 |
243 | def get_page_content(self, url):
244 | try:
245 | with contextlib.closing(request.urlopen(url)) as fd:
246 | return fd.read()
247 | except error.URLError:
248 | return None
249 |
250 |
251 | img = ('
')
253 | warning_header = '{} **Warning** {}'.format(img, img)
254 | warning_summary = warning_header + '\n\n%s'
255 |
256 |
257 | def extract_globals_from_payload(payload):
258 | if "issue" in payload:
259 | owner = payload['repository']['owner']['login']
260 | repo = payload['repository']['name']
261 | issue = str(payload['issue']['number'])
262 | else:
263 | owner = payload['pull_request']['base']['repo']['owner']['login']
264 | repo = payload['pull_request']['base']['repo']['name']
265 | if "number" in payload:
266 | issue = str(payload["number"])
267 | else:
268 | issue = str(payload["pull_request"]["number"])
269 | return (owner, repo, issue)
270 |
271 |
272 | def handle_payload(api, payload, handlers=None):
273 | if not handlers:
274 | modules, handlers = eventhandler.get_handlers()
275 | for handler in handlers:
276 | handler.handle_payload(api, payload)
277 | warnings = eventhandler.get_warnings()
278 | if warnings:
279 | formatted_warnings = '\n'.join(map(lambda x: '* ' + x, warnings))
280 | api.post_comment(warning_summary % formatted_warnings)
281 |
282 |
283 | if __name__ == "__main__":
284 | print("Content-Type: text/html;charset=utf-8")
285 | print()
286 |
287 | config = RawConfigParser()
288 | config.read('./config')
289 | user = config.get('github', 'user')
290 | token = config.get('github', 'token')
291 |
292 | post = cgi.FieldStorage()
293 | payload_raw = post.getfirst("payload", '')
294 | payload = json.loads(payload_raw)
295 |
296 | handle_payload(GithubAPIProvider(payload, user, token), payload)
297 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flake8 == 2.5.4
2 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function
2 | from copy import deepcopy
3 |
4 | import json
5 | import os
6 | import sys
7 | import traceback
8 |
9 | import eventhandler
10 | from json_cleanup import JsonCleaner
11 | from newpr import APIProvider, handle_payload
12 |
13 |
14 | class TestAPIProvider(APIProvider):
15 | def __init__(self, payload, user, new_contributor, labels, assignee,
16 | diff=None, pull_request=""):
17 | super(TestAPIProvider, self).__init__(payload, user)
18 | self.new_contributor = new_contributor
19 | self.comments_posted = []
20 | self.labels = labels
21 | self.assignee = assignee
22 | self.diff = diff
23 | self.pull_request = pull_request
24 | self.repo = str(self.repo) # workaround for testing
25 |
26 | def is_new_contributor(self, username):
27 | return self.new_contributor
28 |
29 | def post_comment(self, body):
30 | self.comments_posted += [body]
31 |
32 | def add_label(self, label):
33 | self.labels += [label]
34 |
35 | def remove_label(self, label):
36 | self.labels.remove(label)
37 |
38 | def get_labels(self):
39 | return self.labels
40 |
41 | def get_diff(self):
42 | if not self.diff:
43 | return ''
44 | return self.diff.decode('utf-8')
45 |
46 | def get_pull(self):
47 | return json.loads(self.pull_request)
48 |
49 | def set_assignee(self, assignee):
50 | self.assignee = assignee
51 |
52 | def get_page_content(self, path):
53 | with open(path) as fd:
54 | return fd.read()
55 |
56 |
57 | def create_test(filename, initial, expected,
58 | payload_wrapper, to_clean, clean_dict):
59 | initial_values = {
60 | 'new_contributor': initial.get('new_contributor', False),
61 | 'labels': initial.get('labels', []),
62 | 'diff': initial.get('diff', None),
63 | 'pull_request': json.dumps(initial.get('pull_request', {})),
64 | 'assignee': initial.get('assignee', None),
65 | }
66 |
67 | return {
68 | 'filename': filename,
69 | 'initial': initial_values,
70 | 'expected': expected,
71 | 'wrapper': payload_wrapper,
72 | 'clean': to_clean,
73 | 'dict': clean_dict,
74 | }
75 |
76 |
77 | def run_tests(tests, warn=True, overwrite=False):
78 | failed, dirty = 0, 0
79 | for handler, test in tests:
80 | eventhandler.reset_test_state()
81 |
82 | try:
83 | # print(test["filename"])
84 | initial, expected, = test['initial'], test['expected']
85 | wrapper = test['wrapper']
86 | payload = wrapper.json['payload']
87 | if initial['diff']:
88 | diff = initial['diff'].encode('utf-8')
89 | else:
90 | diff = None
91 | api = TestAPIProvider(payload,
92 | 'highfive',
93 | initial['new_contributor'],
94 | initial['labels'],
95 | initial['assignee'],
96 | diff,
97 | initial['pull_request'])
98 | handle_payload(api, payload, [handler])
99 |
100 | if 'comments' in expected:
101 | assert len(api.comments_posted) == expected['comments'], \
102 | "%d == %d" % (len(api.comments_posted),
103 | expected['comments'])
104 | if 'labels' in expected:
105 | assert api.labels == expected['labels'], \
106 | "%s == %s" % (api.labels, expected['labels'])
107 | if 'assignee' in expected:
108 | assert api.assignee == expected['assignee'], \
109 | "%s == %s" % (api.assignee, expected['assignee'])
110 |
111 | # If this is the last test in the file, then it's time for cleanup
112 | if test['clean']:
113 | cleaned = wrapper.clean(warn)
114 |
115 | if wrapper.unused and not overwrite:
116 | error = '\033[91m%s\033[0m: The file has %s unused nodes'
117 | print(error % (test['filename'], wrapper.unused))
118 | dirty += 1
119 |
120 | if overwrite: # useful for cleaning up the tests locally
121 | clean_dict = test['dict']
122 | clean_dict['payload'] = cleaned['payload']
123 | with open(test['filename'], 'w') as fd:
124 | json.dump(clean_dict, fd, indent=2)
125 | error = '\033[91m%s\033[0m: Rewrote the JSON file'
126 | print(error % test['filename'])
127 |
128 | except AssertionError as error:
129 | _, _, tb = sys.exc_info()
130 | traceback.print_tb(tb) # Fixed format
131 | tb_info = traceback.extract_tb(tb)
132 | filename, line, func, text = tb_info[-1]
133 | error_template = '\033[91m{}\033[0m: An error occurred on ' + \
134 | 'line {} in statement {}'
135 | print(error_template.format(test['filename'], line, text))
136 | print(error)
137 | failed += 1
138 |
139 | return failed, dirty
140 |
141 |
142 | def register_tests(path):
143 | tests_location = os.path.join(os.path.dirname(path), 'tests')
144 | if not os.path.isdir(tests_location):
145 | return
146 |
147 | tests = [os.path.join(tests_location, f)
148 | for f in os.listdir(tests_location)
149 | if f.endswith('.json')]
150 |
151 | for testfile in tests:
152 | with open(testfile) as f:
153 | contents = json.load(f)
154 |
155 | # backup the initial/expected values so that we can restore later
156 | # (if we plan to fix the JSON files)
157 | clean_dict = {'initial': deepcopy(contents['initial']),
158 | 'expected': deepcopy(contents['expected'])}
159 |
160 | initial_values = contents['initial']
161 | expected_values = contents['expected']
162 | if not isinstance(initial_values, list):
163 | assert not isinstance(expected_values, list)
164 | initial_values = [initial_values]
165 | expected_values = [expected_values]
166 |
167 | wrapper = JsonCleaner({'payload': contents['payload']})
168 | for i, (initial, expected) in enumerate(zip(initial_values,
169 | expected_values)):
170 | min_length = min(len(initial_values), len(expected_values))
171 | is_last_test = i == min_length - 1
172 | yield create_test(testfile, initial, expected,
173 | wrapper, is_last_test, clean_dict)
174 |
175 |
176 | def setup_tests():
177 | (modules, handlers) = eventhandler.get_handlers()
178 | tests = []
179 | for module, handler in zip(modules, handlers):
180 | for test in register_tests(module[1]):
181 | tests.append((handler, test))
182 | return tests
183 |
184 |
185 | if __name__ == "__main__":
186 | args = ' '.join(sys.argv)
187 | overwrite = True if 'write' in args else False
188 |
189 | tests = setup_tests()
190 | failed, dirty = run_tests(tests, not overwrite, overwrite)
191 |
192 | print('Ran %d tests, %d failed, %d file(s) dirty' %
193 | (len(tests), failed, dirty))
194 |
195 | if failed or dirty:
196 | if dirty:
197 | print('Run `python %s write` to cleanup the dirty files' % args)
198 | sys.exit(1)
199 |
--------------------------------------------------------------------------------