-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMainController.py
344 lines (297 loc) · 15.5 KB
/
MainController.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
"""
#
# Cherry-Picker
#
# Copyright (C) 2020 by Leonardo Coelho: https://www.linkedin.com/in/leo-coelho/
#
################################################################################
#
# This file is subject to the terms and conditions defined in
# file 'LICENSE', which is part of this source code package.
#
"""
import json
import os
import re
from github import Github, GithubException
from jira import JIRA, JIRAError
import git as g
from requests.exceptions import MissingSchema
import JIRAUtils
from GUI import GUI
class MainController:
def __init__(self):
# - - - - - - - - - - - - - - - - - - - - -
# JIRA Credentials
self.jira_url = None
self.jira_username = None
self.jira_password = None
# - - - - - - - - - - - - - - - - - - - - -
# GitHub Credential
self.github_username = None
self.github_password = None
# - - - - - - - - - - - - - - - - - - - - -
# Backport Fields
self.service_pack = None
self.assignee = None
self.base_folder = None
# - - - - - - - - - - - - - - - - - - - - -
# Merge Masters
self.master1 = None
self.master2 = None
self.jira_connection = None
self.github_connection = None
self.backports = None
# Create the entire GUI program
self.gui = GUI(self)
# Start the GUI event loop
self.gui.window.mainloop()
# Get SP cases for a given assignee and service pack.
def get_sp_cases(self):
self.gui.clear_logs()
self.jira_url = self.gui.jira_url_input.get().strip()
self.jira_username = self.gui.jira_user_input.get().strip()
self.jira_password = self.gui.jira_password_input.get()
self.service_pack = self.gui.service_pack_input.get().strip()
self.assignee = self.gui.assignee_input.get().strip()
if self.assignee:
self.gui.log_info("Getting " + self.assignee + "'s SP cases for " + self.service_pack + "...")
else:
self.gui.log_info("Getting SP cases for " + self.service_pack + "...")
# Connect to JIRA.
try:
self.gui.log_info("Connecting to JIRA...")
self.jira_connection = JIRA(server=self.jira_url, basic_auth=(self.jira_username, self.jira_password))
except MissingSchema as me:
self.gui.log_error("Unable to connect to JIRA: " + str(me))
return
except JIRAError as je:
self.gui.log_error("Unable to connect to JIRA: " + je.text)
return
# Get Open and In Progress SP cases.
try:
sp_cases = JIRAUtils.get_sp_cases(self.jira_connection, self.service_pack, self.assignee)
except JIRAError as je:
self.gui.log_error("Unable to fetch SP Cases: " + je.text)
return
# Update list of SP cases to be backported.
self.gui.update_sp_list(sp_cases)
self.gui.log_info(str(len(sp_cases)) + " SP cases added.")
# Backport the selected SP cases.
def backport(self):
self.gui.clear_logs()
self.gui.log_info("Starting to Backport...")
# Connect to GitHub.
self.github_username = self.gui.github_user_input.get().strip()
self.github_password = self.gui.github_password_input.get()
try:
self.github_connection = Github(self.github_username, self.github_password)
me = self.github_connection.get_user(self.github_username)
except GithubException as ge:
self.gui.log_error("Unable to connect to GitHub: " + ge.data['message'])
self.gui.log_info("Done!")
return
self.base_folder = self.gui.base_folder_input.get().strip()
# Go through all SP cases
sp_keys = [sp.split(' ')[0].replace('[', '').replace(']', '') for sp in self.gui.backports_listbox.get()]
for sp_key in sp_keys:
# Apply the Begin Work transition for the SP case.
issue = self.jira_connection.issue(sp_key)
self.jira_connection.assign_issue(issue, self.jira_username)
if issue.fields.status.name == 'Open':
self.jira_connection.transition_issue(issue.key, '11')
# Get data from the JIRA Developer plugin.
# Get Base Bug commits.
self.gui.log_info("Backporting " + sp_key + "!")
base_bug = JIRAUtils.get_base_bug(self.jira_connection, sp_key)
raw_data = JIRAUtils.get_data(self.jira_connection, base_bug)
repositories = raw_data['detail'][0]['repositories']
rep_names = [rep['name'] for rep in repositories]
# Search for missing commits.
# Find "PR: <git-pr-link>" patterns in JIRA case comments.
jira_comments = [[re.search(r'(?<=PR:).*', body).group(0) for body in
comment.body.encode("ascii", errors="ignore").decode().replace("\r\n", "\n").replace("\r", "\n").split('\n') if
re.search(r'(?<=PR:).*', body) is not None] for comment in
self.jira_connection.issue(base_bug.key).fields.comment.comments]
links_in_comments = [item.strip() for sublist in jira_comments for item in sublist]
# Check if there are missing commits in JIRA Developer plugin.
for rep_name in rep_names:
for not_missing_link in links_in_comments:
# Commits are on JIRA Developer Plugin.
if rep_name in not_missing_link:
break
# Commits are missing
upstream_repo = me.get_repo(rep_name).parent
try:
pr = upstream_repo.get_pull(not_missing_link.split('/')[-1])
except:
continue
for commit in pr.get_commits().get_page(0):
rep_name = commit.html_url.split('/')[4]
sha = commit.html_url.split('/')[-1]
repositories.append({'name': rep_name,
'commits': [
{'message': "Missing Commit", 'id': sha, 'url': commit.html_url,
'authorTimestamp': 1}]})
if not rep_names:
for missing_link in links_in_comments:
# All commits are missing
rep_name = missing_link.split('/')[-3]
pr_nr = missing_link.split('/')[-1]
pr_nr = int(''.join([i for i in pr_nr if i.isdigit()]))
upstream_repo = me.get_repo(rep_name).parent
try:
pr = upstream_repo.get_pull(pr_nr)
except:
continue
for commit in pr.get_commits().get_page(0):
rep_name = commit.html_url.split('/')[4]
sha = commit.html_url.split('/')[-1]
repositories.append({'name': rep_name,
'commits': [
{'message': "Missing Commit", 'id': sha, 'url': commit.html_url,
'authorTimestamp': 1}]})
# Initialize JIRA comment.
jira_comment = "*Attention: This is the outcome of an automated process!*"
jira_comment += "\nPRs:"
# Go through all repositories.
for repository in repositories:
has_merge_conflicts = False
self.gui.log_info("Creating the " + sp_key + " branch in " + repository['name'] + ".")
# Check if we have the repository in place.
repo_path = os.path.join(os.path.normpath(self.base_folder), repository['name'])
if os.path.exists(repo_path):
repo = g.Repo.init(repo_path)
else:
self.gui.log_error("Couldn't find repository in " + repo_path)
continue
# Create SP branch.
git = repo.git
base_version_branch = self.service_pack.split('-')[1].split(' ')[0]
sp_version_branch = self.service_pack.split(' ')[1].replace('(', '').replace(')', '')
git.fetch('--all')
try:
# Make sure we don't have a SP branch on origin.
git.push("origin", '--delete', sp_key)
except g.GitCommandError:
pass
finally:
try:
# Checkout to version branch.
git.checkout(base_version_branch)
except g.GitCommandError as gce:
git.checkout('-b', base_version_branch, 'origin/' + base_version_branch)
# Pull all version branch changes.
git.pull('upstream', base_version_branch)
try:
# Make sure we don't have a SP branch locally.
git.branch("-D", sp_key)
except g.GitCommandError:
pass
finally:
git.checkout('-b', sp_key)
# List of commits to be cherry-picked.
commits = repository['commits']
urls = []
# Order commits by date, so we maintain the chronology of events.
commits.sort(key=sort_by_timestamp)
commit_message = '[' + sp_key + '] ' + self.jira_connection.issue(sp_key).fields.summary
# Go through all commits.
for commit in commits:
# Don't cherry-pick merge PR commits.
if not commit['message'].startswith("Merge pull request"):
# Cherry-pick base case commits.
sha = commit['id']
urls.append(commit['url'])
self.gui.log_info("Cherry-picking " + sha + ".")
try:
git.cherry_pick(sha)
except g.GitCommandError as gce:
# Flag that we have merge conflicts, so we can signalize that on the jira comment later.
has_merge_conflicts = True
self.gui.log_error("Unable to cherry-pick '" + sha + "': " + gce.stderr.strip())
# Delete changes.
git.reset('--hard')
break
# Rename commits with backport message.
git.commit('--amend', '-m', commit_message)
# Proceed with the backport, if we don't have conflicts
base_pr = version_pr = None
if has_merge_conflicts is False:
try:
# Push changes.
self.gui.log_info("Pushing commits to " + sp_key + " branch.")
git.push("origin", sp_key)
except g.GitCommandError as gce:
self.gui.log_error("Unable to push changes to origin " + sp_key + " branch: " + gce.stderr.strip())
git.checkout('master')
git.branch("-D", sp_key)
self.gui.log_info("Done with " + repository['name'] + "!")
# Build PR message.
self.master1 = self.gui.master1_input.get()
self.master2 = self.gui.master2_input.get()
pr_message = "**Attention: This is the outcome of an automated process!**"
pr_message += "\nMerge Masters: " + self.master1 + " and " + self.master2 + "\n"
pr_message += "Cherry-picks:\n"
for url in urls:
pr_message += "* " + url + "\n"
# Build and send Pull Request.
self.gui.log_info("Opening PRs for " + sp_key + ".")
upstream_repo = me.get_repo(repository['name']).parent
# For version branch
try:
upstream_repo.get_branch(base_version_branch)
base_pr = upstream_repo.create_pull(commit_message, pr_message, base_version_branch,
'{}:{}'.format(self.github_username, sp_key), True)
except GithubException as ge:
if ge.status == 422:
self.gui.log_error(
"Unable to submit PR for " + sp_key + " in " + base_version_branch + " branch: " +
ge.data['errors'][0]['message'])
else:
self.gui.log_error(
"Unable to submit PR for " + sp_key + " in " + base_version_branch + " branch: " +
ge.data['message'])
else:
self.gui.log_info("Opened Pull Request in " + base_version_branch + " branch")
# For SP branch
try:
upstream_repo.get_branch(sp_version_branch)
version_pr = upstream_repo.create_pull(commit_message, pr_message, sp_version_branch,
'{}:{}'.format(self.github_username, sp_key), True)
except GithubException as ge:
if ge.status == 422:
self.gui.log_error(
"Unable to submit PR for " + sp_key + " in " + sp_version_branch + " branch: " +
ge.data['errors'][0]['message'])
else:
self.gui.log_warn(
"Unable to submit PR for " + sp_key + " in " + sp_version_branch + " branch: " +
ge.data['message'])
else:
self.gui.log_info("Opened Pull Request in " + sp_version_branch + " branch")
# Delete branch and Move to next repository.
self.gui.log_info("Deleting " + sp_key + " branch...")
git.checkout('master')
git.branch("-D", sp_key)
self.gui.log_info("Done with " + repository['name'] + "!")
# Add PR links in the JIRA case
self.gui.log_info("Adding PR links in " + sp_key + "...")
jira_comment += "\n* " + repository['name'] + ":"
if base_pr:
jira_comment += "\n** " + base_version_branch + ": " + base_pr.html_url
elif has_merge_conflicts:
jira_comment += " There are conflicts that need to be manually treated."
if version_pr:
jira_comment += "\n** " + sp_version_branch + ": " + version_pr.html_url
# Add pull-request-sent label
issue.fields.labels.append(u"pull-request-sent")
issue.update(fields={"labels": issue.fields.labels})
# Move issue to block status
self.jira_connection.transition_issue(sp_key, '61', comment=jira_comment)
# Move to next SP case.
self.gui.log_info("Done with " + sp_key + "!")
def sort_by_timestamp(val):
return val['authorTimestamp']
# Start application
MainController()