]>
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 build_branch ( args
):
167 branch
= datetime
. datetime
. utcnow (). strftime ( args
. branch
). format ( user
= USER
)
169 merge_branch_name
= args
. merge_branch_name
170 if merge_branch_name
is False :
171 merge_branch_name
= branch
174 #Check the label format
175 if re
. search ( r
'\bwip-(.*?)-testing\b' , label
) is None :
176 log
. error ( "Unknown Label ' {lblname} '. Label Format: wip-<name>-testing" . format ( lblname
= label
))
179 #Check if the Label exist in the repo
180 res
= requests
. get ( "https://api.github.com/repos/ {project} / {repo} /labels/ {lblname} " . format ( lblname
= label
, project
= BASE_PROJECT
, repo
= BASE_REPO
), auth
=( USER
, PASSWORD
))
181 if res
. status_code
!= 200 :
182 log
. error ( "Label ' {lblname} ' not found in the repo" . format ( lblname
= label
))
185 G
= git
. Repo ( args
. git
)
187 # First get the latest base branch and PRs from BASE_REMOTE
188 remote
= getattr ( G
. remotes
, BASE_REMOTE
)
192 if args
. pr_label
is not None :
193 if args
. pr_label
== '' or args
. pr_label
. isspace ():
194 log
. error ( "--pr-label must have a non-space value" )
196 payload
= { 'labels' : args
. pr_label
, 'sort' : 'created' , 'direction' : 'desc' }
197 labeled_prs
= requests
. get ( "https://api.github.com/repos/ {project} / {repo} /issues" . format ( project
= BASE_PROJECT
, repo
= BASE_REPO
), auth
=( USER
, PASSWORD
), params
= payload
)
198 if labeled_prs
. status_code
!= 200 :
199 log
. error ( "Failed to load labeled PRs: {}" . format ( labeled_prs
))
201 labeled_prs
= labeled_prs
. json ()
202 if len ( labeled_prs
) == 0 :
203 log
. error ( "Search for PRs matching label '{}' returned no results!" . format ( args
. pr_label
))
205 for pr
in labeled_prs
:
206 if pr
[ 'pull_request' ]:
208 log
. info ( "Adding labeled PR #{} to PR list" . format ( n
))
210 log
. info ( "Will merge PRs: {}" . format ( prs
))
213 log
. info ( "Branch base is HEAD; not checking out!" )
215 log
. info ( "Detaching HEAD onto base: {}" . format ( base
))
217 base_path
= args
. base_path
+ base
218 base
= filter ( lambda r
: r
. path
== base_path
, G
. refs
)[ 0 ]
220 log
. error ( "Branch " + base
+ " does not exist!" )
223 # So we know that we're not on an old test branch, detach HEAD onto ref:
228 log
. info ( "Merging PR # {pr} " . format ( pr
= pr
))
230 remote_ref
= "refs/pull/ {pr} /head" . format ( pr
= pr
)
231 fi
= remote
. fetch ( remote_ref
)
233 log
. error ( "PR {pr} does not exist?" . format ( pr
= pr
))
235 tip
= fi
[ 0 ]. ref
. commit
237 pr_req
= requests
. get ( "https://api.github.com/repos/ceph/ceph/pulls/ {pr} " . format ( pr
= pr
), auth
=( USER
, PASSWORD
))
238 if pr_req
. status_code
!= 200 :
239 log
. error ( "PR ' {pr} ' not found: {c} " . format ( pr
= pr
, c
= pr_req
))
242 message
= "Merge PR # %d into %s \n\n * %s : \n " % ( pr
, merge_branch_name
, remote_ref
)
244 for commit
in G
. iter_commits ( rev
= "HEAD.." + str ( tip
)):
245 message
= message
+ ( " \t %s \n " % commit
. message
. split ( ' \n ' , 1 )[ 0 ])
246 # Get tracker issues / bzs cited so the PTL can do updates
247 short
= commit
. hexsha
[: 8 ]
248 for m
in BZ_MATCH
. finditer ( commit
. message
):
249 log
. info ( "[ {sha1} ] BZ cited: {cite} " . format ( sha1
= short
, cite
= m
. group ( 1 )))
250 for m
in TRACKER_MATCH
. finditer ( commit
. message
):
251 log
. info ( "[ {sha1} ] Ceph tracker cited: {cite} " . format ( sha1
= short
, cite
= m
. group ( 1 )))
253 message
= message
+ " \n "
255 comments
= requests
. get ( "https://api.github.com/repos/ {project} / {repo} /issues/ {pr} /comments" . format ( pr
= pr
, project
= BASE_PROJECT
, repo
= BASE_REPO
), auth
=( USER
, PASSWORD
))
256 if comments
. status_code
!= 200 :
257 log
. error ( "PR ' {pr} ' not found: {c} " . format ( pr
= pr
, c
= comments
))
260 reviews
= requests
. get ( "https://api.github.com/repos/ {project} / {repo} /pulls/ {pr} /reviews" . format ( pr
= pr
, project
= BASE_PROJECT
, repo
= BASE_REPO
), auth
=( USER
, PASSWORD
))
261 if reviews
. status_code
!= 200 :
262 log
. error ( "PR ' {pr} ' not found: {c} " . format ( pr
= pr
, c
= comments
))
265 review_comments
= requests
. get ( "https://api.github.com/repos/ {project} / {repo} /pulls/ {pr} /comments" . format ( pr
= pr
, project
= BASE_PROJECT
, repo
= BASE_REPO
), auth
=( USER
, PASSWORD
))
266 if review_comments
. status_code
!= 200 :
267 log
. error ( "PR ' {pr} ' not found: {c} " . format ( pr
= pr
, c
= comments
))
271 for comment
in [ pr_req
. json ()]+ comments
. json ()+ reviews
. json ()+ review_comments
. json ():
272 body
= comment
[ "body" ]
274 url
= comment
[ "html_url" ]
275 for m
in BZ_MATCH
. finditer ( body
):
276 log
. info ( "[ {url} ] BZ cited: {cite} " . format ( url
= url
, cite
= m
. group ( 1 )))
277 for m
in TRACKER_MATCH
. finditer ( body
):
278 log
. info ( "[ {url} ] Ceph tracker cited: {cite} " . format ( url
= url
, cite
= m
. group ( 1 )))
279 for indication
in INDICATIONS
:
280 for cap
in indication
. findall ( comment
[ "body" ]):
283 new_new_contributors
= {}
284 for review
in reviews
. json ():
285 if review
[ "state" ] == "APPROVED" :
286 user
= review
[ "user" ][ "login" ]
288 indications
. add ( "Reviewed-by: " + CONTRIBUTORS
[ user
])
289 except KeyError as e
:
291 indications
. add ( "Reviewed-by: " + NEW_CONTRIBUTORS
[ user
])
292 except KeyError as e
:
294 name
= raw_input ( "Need name for contributor \" %s \" (use ^D to skip); Reviewed-by: " % user
)
298 NEW_CONTRIBUTORS
[ user
] = name
299 new_new_contributors
[ user
] = name
300 indications
. add ( "Reviewed-by: " + name
)
301 except EOFError as e
:
304 for indication
in indications
:
305 message
= message
+ indication
+ " \n "
307 G
. git
. merge ( tip
. hexsha
, '--no-ff' , m
= message
)
309 if new_new_contributors
:
310 # Check out the PR, add a commit adding to .githubmap
311 log
. info ( "adding new contributors to githubmap in merge commit" )
312 with
open ( git_dir
+ "/.githubmap" , "a" ) as f
:
313 for c
in new_new_contributors
:
314 f
. write ( " %s %s \n " % ( c
, new_new_contributors
[ c
]))
315 G
. index
. add ([ ".githubmap" ])
316 G
. git
. commit ( "--amend" , "--no-edit" )
319 req
= requests
. 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
))
320 if req
. status_code
!= 200 :
321 log
. error ( "PR # %d could not be labeled %s : %s " % ( pr
, label
, req
))
323 log
. info ( "Labeled PR # {pr} {label} " . format ( pr
= pr
, label
= label
))
325 # If the branch is 'HEAD', leave HEAD detached (but use "master" for commit message)
327 log
. info ( "Leaving HEAD detached; no branch anchors your commits" )
329 created_branch
= False
331 G
. head
. reference
= G
. create_head ( branch
)
332 log
. info ( "Checked out new branch {branch} " . format ( branch
= branch
))
333 created_branch
= True
335 G
. head
. reference
= G
. create_head ( branch
, force
= True )
336 log
. info ( "Checked out branch {branch} " . format ( branch
= branch
))
339 # tag it for future reference.
340 tag
= "testing/ %s " % branch
341 git
. refs
. tag
. Tag
. create ( G
, tag
)
342 log
. info ( "Created tag %s " % tag
)
345 parser
= argparse
. ArgumentParser ( description
= "Ceph PTL tool" )
346 default_base
= 'master'
347 default_branch
= TEST_BRANCH
349 if len ( sys
. argv
) > 1 and sys
. argv
[ 1 ] in SPECIAL_BRANCHES
:
351 default_branch
= 'HEAD' # Leave HEAD detached
352 default_base
= default_branch
353 default_label
= False
356 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)' )
357 parser
. add_argument ( '--merge-branch-name' , dest
= 'merge_branch_name' , action
= 'store' , default
= False , help = 'name of the branch for merge messages' )
358 parser
. add_argument ( '--base' , dest
= 'base' , action
= 'store' , default
= default_base
, help = 'base for branch' )
359 parser
. add_argument ( '--base-path' , dest
= 'base_path' , action
= 'store' , default
= BASE_PATH
, help = 'base for branch' )
360 parser
. add_argument ( '--git-dir' , dest
= 'git' , action
= 'store' , default
= git_dir
, help = 'git directory' )
361 parser
. add_argument ( '--label' , dest
= 'label' , action
= 'store' , default
= default_label
, help = 'label PRs for testing' )
362 parser
. add_argument ( '--pr-label' , dest
= 'pr_label' , action
= 'store' , help = 'label PRs for testing' )
363 parser
. add_argument ( 'prs' , metavar
= "PR" , type = int , nargs
= '*' , help = 'Pull Requests to merge' )
364 args
= parser
. parse_args ( argv
)
365 return build_branch ( args
)
367 if __name__
== "__main__" :