1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, # You can obtain one at http://mozilla.org/MPL/2.0/.
5 from __future__
import absolute_import
, unicode_literals
14 from mach
.decorators
import (
19 import mozpack
.path
as mozpath
23 GITHUB_ROOT
= "https://github.com/"
26 "github": "servo/webrender",
28 "bugzilla_product": "Core",
29 "bugzilla_component": "Graphics: WebRender",
32 "github": "gfx-rs/wgpu",
34 "bugzilla_product": "Core",
35 "bugzilla_component": "Graphics: WebGPU",
38 "github": "firefox-devtools/debugger",
39 "path": "devtools/client/debugger",
40 "bugzilla_product": "DevTools",
41 "bugzilla_component": "Debugger",
49 description
="Import a pull request from Github to the local repo.",
51 @CommandArgument("-b", "--bug-number", help="Bug number to use in the commit messages.")
55 help="Bugzilla API token used to file a new bug if no bug number is provided.",
57 @CommandArgument("-r", "--reviewer", help="Reviewer nick to apply to commit messages.")
60 help="URL to the pull request to import (e.g. "
61 "https://github.com/servo/webrender/pull/3665).",
74 for r
in PR_REPOSITORIES
.values():
75 if pull_request
.startswith(GITHUB_ROOT
+ r
["github"] + "/pull/"):
76 # sanitize URL, dropping anything after the PR number
77 pr_number
= int(re
.search("/pull/([0-9]+)", pull_request
).group(1))
78 pull_request
= GITHUB_ROOT
+ r
["github"] + "/pull/" + str(pr_number
)
82 if repository
is None:
87 "The pull request URL was not recognized; add it to the list of "
88 "recognized repos in PR_REPOSITORIES in %s" % __file__
,
95 {"pr_url": pull_request
},
96 "Attempting to import {pr_url}",
100 for f
in command_context
.repository
.get_changed_files(mode
="all")
101 if f
.startswith(repository
["path"])
108 "Local {path} tree is dirty; aborting!",
111 target_dir
= mozpath
.join(
112 command_context
.topsrcdir
, os
.path
.normpath(repository
["path"])
115 if bug_number
is None:
116 if bugzilla_token
is None:
121 "No bug number or bugzilla API token provided; bug number will not "
122 "be added to commit messages.",
125 bug_number
= _file_bug(
126 command_context
, bugzilla_token
, repository
, pr_number
128 elif bugzilla_token
is not None:
133 "Providing a bugzilla token is unnecessary when a bug number is provided. "
134 "Using bug number; ignoring token.",
137 pr_patch
= requests
.get(pull_request
+ ".patch")
138 pr_patch
.raise_for_status()
139 for patch
in _split_patches(pr_patch
.content
, bug_number
, pull_request
, reviewer
):
144 "Processing commit [{commit_summary}] by [{author}] at [{date}]",
146 patch_cmd
= subprocess
.Popen(
147 ["patch", "-p1", "-s"], stdin
=subprocess
.PIPE
, cwd
=target_dir
149 patch_cmd
.stdin
.write(patch
["diff"].encode("utf-8"))
150 patch_cmd
.stdin
.close()
152 if patch_cmd
.returncode
!= 0:
157 'Error applying diff from commit via "patch -p1 -s". Aborting...',
159 sys
.exit(patch_cmd
.returncode
)
160 command_context
.repository
.commit(
161 patch
["commit_msg"], patch
["author"], patch
["date"], [target_dir
]
163 command_context
.log(logging
.INFO
, "commit_pass", {}, "Committed successfully.")
166 def _file_bug(command_context
, token
, repo
, pr_number
):
170 "https://bugzilla.mozilla.org/rest/bug?api_key=%s" % token
,
172 "product": repo
["bugzilla_product"],
173 "component": repo
["bugzilla_component"],
174 "summary": "Land %s#%s in mozilla-central" % (repo
["github"], pr_number
),
175 "version": "unspecified",
178 bug
.raise_for_status()
179 command_context
.log(logging
.DEBUG
, "new_bug", {}, bug
.content
)
180 bugnumber
= json
.loads(bug
.content
)["id"]
182 logging
.INFO
, "new_bug", {"bugnumber": bugnumber
}, "Filed bug {bugnumber}"
187 def _split_patches(patchfile
, bug_number
, pull_request
, reviewer
):
194 for line
in patchfile
.splitlines():
196 if line
.startswith(b
"From "):
198 elif state
== HEADERS
:
199 patch
+= line
+ b
"\n"
201 state
= STAT_AND_DIFF
202 elif state
== STAT_AND_DIFF
:
203 if line
.startswith(b
"From "):
204 yield _parse_patch(patch
, bug_number
, pull_request
, reviewer
)
208 patch
+= line
+ b
"\n"
210 yield _parse_patch(patch
, bug_number
, pull_request
, reviewer
)
214 def _parse_patch(patch
, bug_number
, pull_request
, reviewer
):
221 parse_policy
= policy
.compat32
.clone(max_line_length
=None)
222 parsed_mail
= email
.message_from_bytes(patch
, policy
=parse_policy
)
224 def header_as_unicode(key
):
225 decoded
= header
.decode_header(parsed_mail
[key
])
226 return str(header
.make_header(decoded
))
228 author
= header_as_unicode("From")
229 date
= header_as_unicode("Date")
230 commit_summary
= header_as_unicode("Subject")
231 email_body
= parsed_mail
.get_payload(decode
=True).decode("utf-8")
232 (commit_body
, diff
) = ("\n" + email_body
).rsplit("\n---\n", 1)
235 if bug_number
is not None:
236 bug_prefix
= "Bug %s - " % bug_number
237 commit_summary
= re
.sub(r
"^\[PATCH[0-9 /]*\] ", bug_prefix
, commit_summary
)
238 if reviewer
is not None:
239 commit_summary
+= " r=" + reviewer
241 commit_msg
= commit_summary
+ "\n"
242 if len(commit_body
) > 0:
243 commit_msg
+= commit_body
+ "\n"
244 commit_msg
+= "\n[import_pr] From " + pull_request
+ "\n"
249 "commit_summary": commit_summary
,
250 "commit_msg": commit_msg
,