##// END OF EJS Templates
Merge pull request #4172 from minrk/backport-pr...
Min RK -
r12436:04176e09 merge
parent child Browse files
Show More
@@ -2,24 +2,37 b''
2 """
2 """
3 Backport pull requests to a particular branch.
3 Backport pull requests to a particular branch.
4
4
5 Usage: backport_pr.py branch PR
5 Usage: backport_pr.py branch [PR]
6
6
7 e.g.:
7 e.g.:
8
8
9 backport_pr.py 0.13.1 123
9 python tools/backport_pr.py 0.13.1 123
10
10
11 to backport PR #123 onto branch 0.13.1
11 to backport PR #123 onto branch 0.13.1
12
12
13 or
14
15 python tools/backport_pr.py 1.x
16
17 to see what PRs are marked for backport that have yet to be applied.
18
13 """
19 """
14
20
15 from __future__ import print_function
21 from __future__ import print_function
16
22
17 import os
23 import os
24 import re
18 import sys
25 import sys
26
19 from subprocess import Popen, PIPE, check_call, check_output
27 from subprocess import Popen, PIPE, check_call, check_output
20 from urllib import urlopen
28 from urllib import urlopen
21
29
22 from gh_api import get_pull_request, get_pull_request_files
30 from gh_api import (
31 get_issues_list,
32 get_pull_request,
33 get_pull_request_files,
34 is_pull_request,
35 )
23
36
24 def find_rejects(root='.'):
37 def find_rejects(root='.'):
25 for dirname, dirs, files in os.walk(root):
38 for dirname, dirs, files in os.walk(root):
@@ -84,9 +97,47 b" def backport_pr(branch, num, project='ipython/ipython'):"
84
97
85 return 0
98 return 0
86
99
100 backport_re = re.compile(r"[Bb]ackport.*?(\d+)")
101
102 def already_backported(branch, since_tag=None):
103 """return set of PRs that have been backported already"""
104 if since_tag is None:
105 since_tag = check_output(['git','describe', branch, '--abbrev=0']).decode('utf8').strip()
106 cmd = ['git', 'log', '%s..%s' % (since_tag, branch), '--oneline']
107 lines = check_output(cmd).decode('utf8')
108 return set(int(num) for num in backport_re.findall(lines))
109
110 def should_backport(labels):
111 """return set of PRs marked for backport"""
112 issues = get_issues_list("ipython/ipython",
113 labels=labels,
114 state='closed',
115 auth=True,
116 )
117 should_backport = set()
118 for issue in issues:
119 if not is_pull_request(issue):
120 continue
121 pr = get_pull_request("ipython/ipython", issue['number'], auth=True)
122 if not pr['merged']:
123 print ("Marked PR closed without merge: %i" % pr['number'])
124 continue
125 should_backport.add(pr['number'])
126 return should_backport
127
87 if __name__ == '__main__':
128 if __name__ == '__main__':
88 if len(sys.argv) < 3:
129
130 if len(sys.argv) < 2:
89 print(__doc__)
131 print(__doc__)
90 sys.exit(1)
132 sys.exit(1)
91
133
134 if len(sys.argv) < 3:
135 branch = sys.argv[1]
136 already = already_backported(branch)
137 should = should_backport("backport-1.1")
138 print ("The following PRs should be backported:")
139 for pr in should.difference(already):
140 print (pr)
141 sys.exit(0)
142
92 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
143 sys.exit(backport_pr(sys.argv[1], int(sys.argv[2])))
@@ -119,12 +119,13 b' def get_pull_request_files(project, num, auth=False):'
119 element_pat = re.compile(r'<(.+?)>')
119 element_pat = re.compile(r'<(.+?)>')
120 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
120 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
121
121
122 def get_paged_request(url, headers=None):
122 def get_paged_request(url, headers=None, **params):
123 """get a full list, handling APIv3's paging"""
123 """get a full list, handling APIv3's paging"""
124 results = []
124 results = []
125 params.setdefault("per_page", 100)
125 while True:
126 while True:
126 print("fetching %s" % url, file=sys.stderr)
127 print("fetching %s with %s" % (url, params), file=sys.stderr)
127 response = requests.get(url, headers=headers)
128 response = requests.get(url, headers=headers, params=params)
128 response.raise_for_status()
129 response.raise_for_status()
129 results.extend(response.json())
130 results.extend(response.json())
130 if 'next' in response.links:
131 if 'next' in response.links:
@@ -133,28 +134,32 b' def get_paged_request(url, headers=None):'
133 break
134 break
134 return results
135 return results
135
136
136 def get_pulls_list(project, state="closed", auth=False):
137 def get_pulls_list(project, auth=False, **params):
137 """get pull request list
138 """get pull request list"""
138 """
139 params.setdefault("state", "closed")
139 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
140 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
140 if auth:
141 if auth:
141 headers = make_auth_header()
142 headers = make_auth_header()
142 else:
143 else:
143 headers = None
144 headers = None
144 pages = get_paged_request(url, headers=headers)
145 pages = get_paged_request(url, headers=headers, params=params)
145 return pages
146 return pages
146
147
147 def get_issues_list(project, state="closed", auth=False):
148 def get_issues_list(project, auth=False, **params):
148 """get pull request list
149 """get issues list"""
149 """
150 params.setdefault("state", "closed")
150 url = "https://api.github.com/repos/{project}/pulls?state={state}&per_page=100".format(project=project, state=state)
151 url = "https://api.github.com/repos/{project}/issues".format(project=project)
151 if auth:
152 if auth:
152 headers = make_auth_header()
153 headers = make_auth_header()
153 else:
154 else:
154 headers = None
155 headers = None
155 pages = get_paged_request(url, headers=headers)
156 pages = get_paged_request(url, headers=headers, **params)
156 return pages
157 return pages
157
158
159 def is_pull_request(issue):
160 """Return True if the given issue is a pull request."""
161 return bool(issue.get('pull_request', {}).get('html_url', None))
162
158 # encode_multipart_formdata is from urllib3.filepost
163 # encode_multipart_formdata is from urllib3.filepost
159 # The only change is to iter_fields, to enforce S3's required key ordering
164 # The only change is to iter_fields, to enforce S3's required key ordering
160
165
@@ -13,7 +13,7 b' import sys'
13
13
14 from datetime import datetime, timedelta
14 from datetime import datetime, timedelta
15 from subprocess import check_output
15 from subprocess import check_output
16 from gh_api import get_paged_request, make_auth_header, get_pull_request
16 from gh_api import get_paged_request, make_auth_header, get_pull_request, is_pull_request
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Globals
19 # Globals
@@ -50,12 +50,6 b' def issues2dict(issues):'
50 idict[i['number']] = i
50 idict[i['number']] = i
51 return idict
51 return idict
52
52
53
54 def is_pull_request(issue):
55 """Return True if the given issue is a pull request."""
56 return bool(issue.get('pull_request', {}).get('html_url', None))
57
58
59 def split_pulls(all_issues, project="ipython/ipython"):
53 def split_pulls(all_issues, project="ipython/ipython"):
60 """split a list of closed issues into non-PR Issues and Pull Requests"""
54 """split a list of closed issues into non-PR Issues and Pull Requests"""
61 pulls = []
55 pulls = []
General Comments 0
You need to be logged in to leave comments. Login now