]>
git.proxmox.com Git - ceph.git/blob - ceph/src/script/ptl-tool.py
5 # This tool's purpose is to make it easier to merge PRs into test branches and
6 # into master. Make sure you generate a Personal access token in GitHub and
7 # add it your ~/.github.key.
9 # Because developers often have custom names for the ceph upstream remote
10 # (https://github.com/ceph/ceph.git), You will probably want to export the
11 # PTL_TOOL_BASE_PATH environment variable in your shell rc files before using
14 # export PTL_TOOL_BASE_PATH=refs/remotes/<remotename>/
16 # and PTL_TOOL_BASE_REMOTE as the name of your Ceph upstream remote (default: "upstream"):
18 # export PTL_TOOL_BASE_REMOTE=<remotename>
21 # ** Here are some basic exmples to get started: **
23 # Merging all PRs labeled 'wip-pdonnell-testing' into a new test branch:
25 # $ src/script/ptl-tool.py --pr-label wip-pdonnell-testing
26 # Adding labeled PR #18805 to PR list
27 # Adding labeled PR #18774 to PR list
28 # Adding labeled PR #18600 to PR list
29 # Will merge PRs: [18805, 18774, 18600]
30 # Detaching HEAD onto base: master
34 # Checked out new branch wip-pdonnell-testing-20171108.054517
35 # Created tag testing/wip-pdonnell-testing-20171108.054517
38 # Merging all PRs labeled 'wip-pdonnell-testing' into master:
40 # $ src/script/ptl-tool.py --pr-label wip-pdonnell-testing --branch master
41 # Adding labeled PR #18805 to PR list
42 # Adding labeled PR #18774 to PR list
43 # Adding labeled PR #18600 to PR list
44 # Will merge PRs: [18805, 18774, 18600]
45 # Detaching HEAD onto base: master
49 # Checked out branch master
52 # $ git push upstream master
56 # Merging PR #1234567 and #2345678 into a new test branch with a testing label added to the PR:
58 # $ src/script/ptl-tool.py 1234567 2345678 --label wip-pdonnell-testing
59 # Detaching HEAD onto base: master
61 # Labeled PR #1234567 wip-pdonnell-testing
63 # Labeled PR #2345678 wip-pdonnell-testing
64 # Deleted old test branch wip-pdonnell-testing-20170928
65 # Created branch wip-pdonnell-testing-20170928
66 # Created tag testing/wip-pdonnell-testing-20170928_03
69 # Merging PR #1234567 into master leaving a detached HEAD (i.e. do not update your repo's master branch) and do not label:
71 # $ src/script/ptl-tool.py --branch HEAD --merge-branch-name master 1234567
72 # Detaching HEAD onto base: master
74 # Leaving HEAD detached; no branch anchors your commits
77 # $ git push upstream HEAD:master
80 # Merging PR #12345678 into luminous leaving a detached HEAD (i.e. do not update your repo's master branch) and do not label:
82 # $ src/script/ptl-tool.py --base luminous --branch HEAD --merge-branch-name luminous 12345678
83 # Detaching HEAD onto base: luminous
84 # Merging PR #12345678
85 # Leaving HEAD detached; no branch anchors your commits
87 # Now push to luminous:
88 # $ git push upstream HEAD:luminous
91 # Merging all PRs labelled 'wip-pdonnell-testing' into master leaving a detached HEAD:
93 # $ src/script/ptl-tool.py --base master --branch HEAD --merge-branch-name master --pr-label wip-pdonnell-testing
94 # Adding labeled PR #18192 to PR list
95 # Will merge PRs: [18192]
96 # Detaching HEAD onto base: master
98 # Leaving HEAD detached; no branch anchors your commit
102 # Look for check failures?
103 # redmine issue update: http://www.redmine.org/projects/redmine/wiki/Rest_Issues
118 from os
. path
import expanduser
120 log
= logging
. getLogger ( __name__
)
121 log
. addHandler ( logging
. StreamHandler ())
122 log
. setLevel ( logging
. INFO
)
124 BASE_PROJECT
= os
. getenv ( "PTL_TOOL_BASE_PROJECT" , "ceph" )
125 BASE_REPO
= os
. getenv ( "PTL_TOOL_BASE_REPO" , "ceph" )
126 BASE_REMOTE
= os
. getenv ( "PTL_TOOL_BASE_REMOTE" , "upstream" )
127 BASE_PATH
= os
. getenv ( "PTL_TOOL_BASE_PATH" , "refs/remotes/upstream/" )
128 GITDIR
= os
. getenv ( "PTL_TOOL_GITDIR" , "." )
129 USER
= os
. getenv ( "PTL_TOOL_USER" , getpass
. getuser ())
130 with
open ( expanduser ( "~/.github.key" )) as f
:
131 PASSWORD
= f
. read (). strip ()
132 TEST_BRANCH
= os
. getenv ( "PTL_TOOL_TEST_BRANCH" , "wip- {user} -testing-%Y%m %d .%H%M%S" )
134 SPECIAL_BRANCHES
= ( 'master' , 'luminous' , 'jewel' , 'HEAD' )
137 re
. compile ( "(Reviewed-by: .+ <[\w@.-]+>)" , re
. IGNORECASE
),
138 re
. compile ( "(Acked-by: .+ <[\w@.-]+>)" , re
. IGNORECASE
),
139 re
. compile ( "(Tested-by: .+ <[\w@.-]+>)" , re
. IGNORECASE
),
142 # find containing git dir
145 while not os
. path
. exists ( git_dir
+ '/.git' ):
152 NEW_CONTRIBUTORS
= {}
153 with codecs
. open ( git_dir
+ "/.githubmap" , encoding
= 'utf-8' ) as f
:
154 comment
= re
. compile ( "\s*#" )
155 patt
= re
. compile ( "([\w-]+)\s+(.*)" )
157 if comment
. match ( line
):
160 CONTRIBUTORS
[ m
. group ( 1 )] = m
. group ( 2 )
162 BZ_MATCH
= re
. compile ( "(.*https?://bugzilla.redhat.com/.*)" )
163 TRACKER_MATCH
= re
. compile ( "(.*https?://tracker.ceph.com/.*)" )
165 def get ( session
, url
, params
= None , paging
= True ):
168 params
[ 'per_page' ] = 100
170 log
. debug ( f
"Fetching {url} " )
171 response
= session
. get ( url
, auth
=( USER
, PASSWORD
), params
= params
)
172 log
. debug ( f
"Response = {response} ; links = {response.headers.get('link', '')}" )
173 if response
. status_code
!= 200 :
174 log
. error ( f
"Failed to fetch {url} : {response} " )
179 link
= response
. headers
. get ( 'link' , None )
181 while link
is not None and 'next' in link
:
182 log
. debug ( f
"Fetching {url} " )
183 new_params
= dict ( params
)
184 new_params
. update ({ 'page' : page
})
185 response
= session
. get ( url
, auth
=( USER
, PASSWORD
), params
= new_params
)
186 log
. debug ( f
"Response = {response} ; links = {response.headers.get('link', '')}" )
187 if response
. status_code
!= 200 :
188 log
. error ( f
"Failed to fetch {url} : {response} " )
190 yield response
. json ()
191 link
= response
. headers
. get ( 'link' , None )
194 def get_credits ( session
, pr
, pr_req
):
197 log
. debug ( f
"Getting comments for # {pr} " )
198 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /issues/ {pr} /comments"
199 for c
in get ( session
, endpoint
):
202 log
. debug ( f
"Getting reviews for # {pr} " )
203 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /pulls/ {pr} /reviews"
205 for c
in get ( session
, endpoint
):
209 log
. debug ( f
"Getting review comments for # {pr} " )
210 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /pulls/ {pr} /comments"
211 for c
in get ( session
, endpoint
):
215 for comment
in comments
:
216 body
= comment
[ "body" ]
218 url
= comment
[ "html_url" ]
219 for m
in BZ_MATCH
. finditer ( body
):
220 log
. info ( "[ {url} ] BZ cited: {cite} " . format ( url
= url
, cite
= m
. group ( 1 )))
221 for m
in TRACKER_MATCH
. finditer ( body
):
222 log
. info ( "[ {url} ] Ceph tracker cited: {cite} " . format ( url
= url
, cite
= m
. group ( 1 )))
223 for indication
in INDICATIONS
:
224 for cap
in indication
. findall ( comment
[ "body" ]):
227 new_new_contributors
= {}
228 for review
in reviews
:
229 if review
[ "state" ] == "APPROVED" :
230 user
= review
[ "user" ][ "login" ]
232 credits
. add ( "Reviewed-by: " + CONTRIBUTORS
[ user
])
233 except KeyError as e
:
235 credits
. add ( "Reviewed-by: " + NEW_CONTRIBUTORS
[ user
])
236 except KeyError as e
:
238 name
= input ( "Need name for contributor \" %s \" (use ^D to skip); Reviewed-by: " % user
)
242 NEW_CONTRIBUTORS
[ user
] = name
243 new_new_contributors
[ user
] = name
244 credits
. add ( "Reviewed-by: " + name
)
245 except EOFError as e
:
248 return " \n " . join ( credits
), new_new_contributors
250 def build_branch ( args
):
252 branch
= datetime
. datetime
. utcnow (). strftime ( args
. branch
). format ( user
= USER
)
254 merge_branch_name
= args
. merge_branch_name
255 if merge_branch_name
is False :
256 merge_branch_name
= branch
258 session
= requests
. Session ()
261 # Check the label format
262 if re
. search ( r
'\bwip-(.*?)-testing\b' , label
) is None :
263 log
. error ( "Unknown Label ' {lblname} '. Label Format: wip-<name>-testing" . format ( lblname
= label
))
266 # Check if the Label exist in the repo
267 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /labels/ {label} "
268 get ( session
, endpoint
, paging
= False )
270 G
= git
. Repo ( args
. git
)
272 # First get the latest base branch and PRs from BASE_REMOTE
273 remote
= getattr ( G
. remotes
, BASE_REMOTE
)
277 if args
. pr_label
is not None :
278 if args
. pr_label
== '' or args
. pr_label
. isspace ():
279 log
. error ( "--pr-label must have a non-space value" )
281 payload
= { 'labels' : args
. pr_label
, 'sort' : 'created' , 'direction' : 'desc' }
282 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /issues"
284 for l
in get ( session
, endpoint
, params
= payload
):
285 labeled_prs
. extend ( l
)
286 if len ( labeled_prs
) == 0 :
287 log
. error ( "Search for PRs matching label '{}' returned no results!" . format ( args
. pr_label
))
289 for pr
in labeled_prs
:
290 if pr
[ 'pull_request' ]:
292 log
. info ( "Adding labeled PR #{} to PR list" . format ( n
))
294 log
. info ( "Will merge PRs: {}" . format ( prs
))
297 log
. info ( "Branch base is HEAD; not checking out!" )
299 log
. info ( "Detaching HEAD onto base: {}" . format ( base
))
301 base_path
= args
. base_path
+ base
302 base
= next ( ref
for ref
in G
. refs
if ref
. path
== base_path
)
303 except StopIteration :
304 log
. error ( "Branch " + base
+ " does not exist!" )
307 # So we know that we're not on an old test branch, detach HEAD onto ref:
312 log
. info ( "Merging PR # {pr} " . format ( pr
= pr
))
314 remote_ref
= "refs/pull/ {pr} /head" . format ( pr
= pr
)
315 fi
= remote
. fetch ( remote_ref
)
317 log
. error ( "PR {pr} does not exist?" . format ( pr
= pr
))
319 tip
= fi
[ 0 ]. ref
. commit
321 endpoint
= f
"https://api.github.com/repos/ {BASE_PROJECT} / {BASE_REPO} /pulls/ {pr} "
322 response
= next ( get ( session
, endpoint
, paging
= False ))
324 message
= "Merge PR # %d into %s \n\n * %s : \n " % ( pr
, merge_branch_name
, remote_ref
)
326 for commit
in G
. iter_commits ( rev
= "HEAD.." + str ( tip
)):
327 message
= message
+ ( " \t %s \n " % commit
. message
. split ( ' \n ' , 1 )[ 0 ])
328 # Get tracker issues / bzs cited so the PTL can do updates
329 short
= commit
. hexsha
[: 8 ]
330 for m
in BZ_MATCH
. finditer ( commit
. message
):
331 log
. info ( "[ {sha1} ] BZ cited: {cite} " . format ( sha1
= short
, cite
= m
. group ( 1 )))
332 for m
in TRACKER_MATCH
. finditer ( commit
. message
):
333 log
. info ( "[ {sha1} ] Ceph tracker cited: {cite} " . format ( sha1
= short
, cite
= m
. group ( 1 )))
335 message
= message
+ " \n "
337 ( addendum
, new_contributors
) = get_credits ( session
, pr
, response
)
340 new_contributors
= []
342 G
. git
. merge ( tip
. hexsha
, '--no-ff' , m
= message
)
345 # Check out the PR, add a commit adding to .githubmap
346 log
. info ( "adding new contributors to githubmap in merge commit" )
347 with
open ( git_dir
+ "/.githubmap" , "a" ) as f
:
348 for c
in new_contributors
:
349 f
. write ( " %s %s \n " % ( c
, new_contributors
[ c
]))
350 G
. index
. add ([ ".githubmap" ])
351 G
. git
. commit ( "--amend" , "--no-edit" )
354 req
= session
. post ( "https://api.github.com/repos/ {project} / {repo} /issues/ {pr} /labels" . format ( pr
= pr
, project
= BASE_PROJECT
, repo
= BASE_REPO
), data
= json
. dumps ([ label
]), auth
=( USER
, PASSWORD
))
355 if req
. status_code
!= 200 :
356 log
. error ( "PR # %d could not be labeled %s : %s " % ( pr
, label
, req
))
358 log
. info ( "Labeled PR # {pr} {label} " . format ( pr
= pr
, label
= label
))
360 # If the branch is 'HEAD', leave HEAD detached (but use "master" for commit message)
362 log
. info ( "Leaving HEAD detached; no branch anchors your commits" )
364 created_branch
= False
366 G
. head
. reference
= G
. create_head ( branch
)
367 log
. info ( "Checked out new branch {branch} " . format ( branch
= branch
))
368 created_branch
= True
370 G
. head
. reference
= G
. create_head ( branch
, force
= True )
371 log
. info ( "Checked out branch {branch} " . format ( branch
= branch
))
374 # tag it for future reference.
375 tag
= "testing/ %s " % branch
376 git
. refs
. tag
. Tag
. create ( G
, tag
)
377 log
. info ( "Created tag %s " % tag
)
380 parser
= argparse
. ArgumentParser ( description
= "Ceph PTL tool" )
381 default_base
= 'master'
382 default_branch
= TEST_BRANCH
384 if len ( sys
. argv
) > 1 and sys
. argv
[ 1 ] in SPECIAL_BRANCHES
:
386 default_branch
= 'HEAD' # Leave HEAD detached
387 default_base
= default_branch
388 default_label
= False
391 parser
. add_argument ( '--branch' , dest
= 'branch' , action
= 'store' , default
= default_branch
, help = 'branch to create ("HEAD" leaves HEAD detached; i.e. no branch is made)' )
392 parser
. add_argument ( '--merge-branch-name' , dest
= 'merge_branch_name' , action
= 'store' , default
= False , help = 'name of the branch for merge messages' )
393 parser
. add_argument ( '--base' , dest
= 'base' , action
= 'store' , default
= default_base
, help = 'base for branch' )
394 parser
. add_argument ( '--base-path' , dest
= 'base_path' , action
= 'store' , default
= BASE_PATH
, help = 'base for branch' )
395 parser
. add_argument ( '--git-dir' , dest
= 'git' , action
= 'store' , default
= git_dir
, help = 'git directory' )
396 parser
. add_argument ( '--label' , dest
= 'label' , action
= 'store' , default
= default_label
, help = 'label PRs for testing' )
397 parser
. add_argument ( '--pr-label' , dest
= 'pr_label' , action
= 'store' , help = 'label PRs for testing' )
398 parser
. add_argument ( '--no-credits' , dest
= 'credits' , action
= 'store_false' , help = 'skip indication search (Reviewed-by, etc.)' )
399 parser
. add_argument ( 'prs' , metavar
= "PR" , type = int , nargs
= '*' , help = 'Pull Requests to merge' )
400 args
= parser
. parse_args ( argv
)
401 return build_branch ( args
)
403 if __name__
== "__main__" :