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/.
12 import mozpack
.path
as mozpath
13 from mach
.decorators
import Command
, CommandArgument
15 GITHUB_ROOT
= "https://github.com/"
18 "github": "servo/webrender",
20 "bugzilla_product": "Core",
21 "bugzilla_component": "Graphics: WebRender",
24 "github": "gfx-rs/wgpu",
26 "bugzilla_product": "Core",
27 "bugzilla_component": "Graphics: WebGPU",
30 "github": "firefox-devtools/debugger",
31 "path": "devtools/client/debugger",
32 "bugzilla_product": "DevTools",
33 "bugzilla_component": "Debugger",
41 description
="Import a pull request from Github to the local repo.",
43 @CommandArgument("-b", "--bug-number", help="Bug number to use in the commit messages.")
47 help="Bugzilla API token used to file a new bug if no bug number is provided.",
49 @CommandArgument("-r", "--reviewer", help="Reviewer nick to apply to commit messages.")
52 help="URL to the pull request to import (e.g. "
53 "https://github.com/servo/webrender/pull/3665).",
66 for r
in PR_REPOSITORIES
.values():
67 if pull_request
.startswith(GITHUB_ROOT
+ r
["github"] + "/pull/"):
68 # sanitize URL, dropping anything after the PR number
69 pr_number
= int(re
.search("/pull/([0-9]+)", pull_request
).group(1))
70 pull_request
= GITHUB_ROOT
+ r
["github"] + "/pull/" + str(pr_number
)
74 if repository
is None:
79 "The pull request URL was not recognized; add it to the list of "
80 "recognized repos in PR_REPOSITORIES in %s" % __file__
,
87 {"pr_url": pull_request
},
88 "Attempting to import {pr_url}",
92 for f
in command_context
.repository
.get_changed_files(mode
="all")
93 if f
.startswith(repository
["path"])
100 "Local {path} tree is dirty; aborting!",
103 target_dir
= mozpath
.join(
104 command_context
.topsrcdir
, os
.path
.normpath(repository
["path"])
107 if bug_number
is None:
108 if bugzilla_token
is None:
113 "No bug number or bugzilla API token provided; bug number will not "
114 "be added to commit messages.",
117 bug_number
= _file_bug(
118 command_context
, bugzilla_token
, repository
, pr_number
120 elif bugzilla_token
is not None:
125 "Providing a bugzilla token is unnecessary when a bug number is provided. "
126 "Using bug number; ignoring token.",
129 pr_patch
= requests
.get(pull_request
+ ".patch")
130 pr_patch
.raise_for_status()
131 for patch
in _split_patches(pr_patch
.content
, bug_number
, pull_request
, reviewer
):
136 "Processing commit [{commit_summary}] by [{author}] at [{date}]",
138 patch_cmd
= subprocess
.Popen(
139 ["patch", "-p1", "-s"], stdin
=subprocess
.PIPE
, cwd
=target_dir
141 patch_cmd
.stdin
.write(patch
["diff"].encode("utf-8"))
142 patch_cmd
.stdin
.close()
144 if patch_cmd
.returncode
!= 0:
149 'Error applying diff from commit via "patch -p1 -s". Aborting...',
151 sys
.exit(patch_cmd
.returncode
)
152 command_context
.repository
.commit(
153 patch
["commit_msg"], patch
["author"], patch
["date"], [target_dir
]
155 command_context
.log(logging
.INFO
, "commit_pass", {}, "Committed successfully.")
158 def _file_bug(command_context
, token
, repo
, pr_number
):
162 "https://bugzilla.mozilla.org/rest/bug?api_key=%s" % token
,
164 "product": repo
["bugzilla_product"],
165 "component": repo
["bugzilla_component"],
166 "summary": "Land %s#%s in mozilla-central" % (repo
["github"], pr_number
),
167 "version": "unspecified",
170 bug
.raise_for_status()
171 command_context
.log(logging
.DEBUG
, "new_bug", {}, bug
.content
)
172 bugnumber
= json
.loads(bug
.content
)["id"]
174 logging
.INFO
, "new_bug", {"bugnumber": bugnumber
}, "Filed bug {bugnumber}"
179 def _split_patches(patchfile
, bug_number
, pull_request
, reviewer
):
186 for line
in patchfile
.splitlines():
188 if line
.startswith(b
"From "):
190 elif state
== HEADERS
:
191 patch
+= line
+ b
"\n"
193 state
= STAT_AND_DIFF
194 elif state
== STAT_AND_DIFF
:
195 if line
.startswith(b
"From "):
196 yield _parse_patch(patch
, bug_number
, pull_request
, reviewer
)
200 patch
+= line
+ b
"\n"
202 yield _parse_patch(patch
, bug_number
, pull_request
, reviewer
)
206 def _parse_patch(patch
, bug_number
, pull_request
, reviewer
):
208 from email
import header
, policy
210 parse_policy
= policy
.compat32
.clone(max_line_length
=None)
211 parsed_mail
= email
.message_from_bytes(patch
, policy
=parse_policy
)
213 def header_as_unicode(key
):
214 decoded
= header
.decode_header(parsed_mail
[key
])
215 return str(header
.make_header(decoded
))
217 author
= header_as_unicode("From")
218 date
= header_as_unicode("Date")
219 commit_summary
= header_as_unicode("Subject")
220 email_body
= parsed_mail
.get_payload(decode
=True).decode("utf-8")
221 (commit_body
, diff
) = ("\n" + email_body
).rsplit("\n---\n", 1)
224 if bug_number
is not None:
225 bug_prefix
= "Bug %s - " % bug_number
226 commit_summary
= re
.sub(r
"^\[PATCH[0-9 /]*\] ", bug_prefix
, commit_summary
)
227 if reviewer
is not None:
228 commit_summary
+= " r=" + reviewer
230 commit_msg
= commit_summary
+ "\n"
231 if len(commit_body
) > 0:
232 commit_msg
+= commit_body
+ "\n"
233 commit_msg
+= "\n[import_pr] From " + pull_request
+ "\n"
238 "commit_summary": commit_summary
,
239 "commit_msg": commit_msg
,