Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(579)

Side by Side Diff: src/scripts/tracker_spreadsheet_sync

Issue 1734001: tracker/spreadsheet tool: Improve tracker to spreadsheet migration. (Closed)
Patch Set: Created 10 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be 4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file. 5 # found in the LICENSE file.
6 6
7 # For Spreadsheets: 7 # For Spreadsheets:
8 try: 8 try:
9 from xml.etree import ElementTree 9 from xml.etree import ElementTree
10 except ImportError: 10 except ImportError:
(...skipping 10 matching lines...) Expand all
21 import gdata.gauth 21 import gdata.gauth
22 import gdata.client 22 import gdata.client
23 import gdata.data 23 import gdata.data
24 import atom.http_core 24 import atom.http_core
25 import atom.core 25 import atom.core
26 26
27 # For this script: 27 # For this script:
28 import getpass 28 import getpass
29 from optparse import OptionParser 29 from optparse import OptionParser
30 import pickle 30 import pickle
31 31 from sets import Set
32 32
33 # Settings 33 # Settings
34 credentials_store = 'creds.dat' 34 credentials_store = 'creds.dat'
35 35
36 class Merger(object): 36 class Merger(object):
37 def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project, 37 def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project,
38 debug): 38 debug, pretend):
39 self.ss_key = ss_key 39 self.ss_key = ss_key
40 self.ss_ws_key = ss_ws_key 40 self.ss_ws_key = ss_ws_key
41 self.tracker_message = tracker_message 41 self.tracker_message = tracker_message
42 self.tracker_project = tracker_project 42 self.tracker_project = tracker_project
43 self.debug_enabled = debug 43 self.debug_enabled = debug
44 self.pretend = pretend
44 self.user_agent = 'adlr-tracker-spreadsheet-merger' 45 self.user_agent = 'adlr-tracker-spreadsheet-merger'
45 self.it_keys = ['id', 'owner', 'status', 'title'] 46 self.it_keys = ['id', 'owner', 'status', 'title']
46 47
47 def debug(self, message): 48 def debug(self, message):
48 """Prints message if debug mode is set.""" 49 """Prints message if debug mode is set."""
49 if self.debug_enabled: 50 if self.debug_enabled:
50 print message 51 print message
51 52
52 def print_feed(self, feed): 53 def print_feed(self, feed):
53 'Handy for debugging' 54 'Handy for debugging'
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
110 feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key) 111 feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key)
111 issues = [] 112 issues = []
112 for entry in feed.entry: 113 for entry in feed.entry:
113 issue = {} 114 issue = {}
114 for key in entry.custom: 115 for key in entry.custom:
115 issue[key] = entry.custom[key].text 116 issue[key] = entry.custom[key].text
116 issue['__raw_entry'] = entry 117 issue['__raw_entry'] = entry
117 issues.append(issue) 118 issues.append(issue)
118 return issues 119 return issues
119 120
120 def fetch_tracker_issues(self): 121 def ids_for_spreadsheet_issues(self, ss_issues):
121 """Fetches all issues matching the query and returns them as an array 122 """Returns a Set of strings, each string an id from ss_issues"""
122 of dictionaries.""" 123 ret = Set()
124 for ss_issue in ss_issues:
125 ret.add(ss_issue['id'])
126 return ret
127
128 def tracker_issues_for_query_feed(self, feed):
129 """Converts a feed object from a query to a list of tracker issue
130 dictionaries."""
131 issues = []
132 for issue in feed.entry:
133 issue_dict = {}
134 issue_dict['labels'] = [label.text for label in issue.label]
135 issue_dict['id'] = issue.id.text.split('/')[-1]
136 issue_dict['title'] = issue.title.text
137 issue_dict['status'] = issue.status.text
138 if issue.owner:
139 issue_dict['owner'] = issue.owner.username.text
140 issues.append(issue_dict)
141 return issues
142
143 def fetch_tracker_issues(self, ss_issues):
144 """Fetches all relevant issues from traacker and returns them as an array
145 of dictionaries. Relevance is:
146 - has an ID that's in ss_issues, OR
147 - (is Area=Installer AND status is open).
148 Open status is one of: Unconfirmed, Untriaged, Available, Assigned,
149 Started, Upstream"""
123 issues = [] 150 issues = []
124 got_results = True 151 got_results = True
125 index = 1 152 index = 1
126 while got_results: 153 while got_results:
127 query = gdata.projecthosting.client.Query(label='Area-Installer', 154 query = gdata.projecthosting.client.Query(label='Area-Installer',
128 max_results=50, 155 max_results=50,
129 start_index=index) 156 start_index=index)
130 feed = self.it_client.get_issues('chromium-os', query=query) 157 feed = self.it_client.get_issues('chromium-os', query=query)
131 if not feed.entry: 158 if not feed.entry:
132 got_results = False 159 got_results = False
133 index = index + len(feed.entry) 160 index = index + len(feed.entry)
134 for issue in feed.entry: 161 issues.extend(self.tracker_issues_for_query_feed(feed))
135 issue_dict = {} 162 # Now, remove issues that are open or in ss_issues.
136 issue_dict['labels'] = [label.text for label in issue.label] 163 ss_ids = self.ids_for_spreadsheet_issues(ss_issues)
137 issue_dict['id'] = issue.id.text.split('/')[-1] 164 open_statuses = ['Unconfirmed', 'Untriaged', 'Available', 'Assigned',
138 issue_dict['title'] = issue.title.text 165 'Started', 'Upstream']
139 issue_dict['status'] = issue.status.text 166 new_issues = []
140 if issue.owner: 167 for issue in issues:
141 issue_dict['owner'] = issue.owner.username.text 168 if issue['status'] in open_statuses or issue['id'] in ss_ids:
142 issues.append(issue_dict) 169 new_issues.append(issue)
170 # Remove id from ss_ids, if it's there
171 ss_ids.discard(issue['id'])
172 issues = new_issues
173
174 # Now, for each ss_id that didn't turn up in the query, explicitly add it
175 for id_ in ss_ids:
176 query = gdata.projecthosting.client.Query(issue_id=id_,
177 max_results=50,
178 start_index=index)
179 feed = self.it_client.get_issues('chromium-os', query=query)
180 if not feed.entry:
181 print 'No result for id', id_
182 continue
183 issues.extend(self.tracker_issues_for_query_feed(feed))
184
143 return issues 185 return issues
144 186
145 def store_creds(self): 187 def store_creds(self):
146 """Stores login credentials to disk.""" 188 """Stores login credentials to disk."""
147 obj = {} 189 obj = {}
148 if self.docs_token: 190 if self.docs_token:
149 obj['docs_token'] = self.docs_token 191 obj['docs_token'] = self.docs_token
150 if self.tracker_token: 192 if self.tracker_token:
151 obj['tracker_token'] = self.tracker_token 193 obj['tracker_token'] = self.tracker_token
152 if self.tracker_user: 194 if self.tracker_user:
(...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after
263 continue 305 continue
264 ss_label = None 306 ss_label = None
265 if value: 307 if value:
266 ss_label = caps_key + value.title() 308 ss_label = caps_key + value.title()
267 t_label = self.label_from_prefix(caps_key, t_issue['labels']) 309 t_label = self.label_from_prefix(caps_key, t_issue['labels'])
268 310
269 if t_label is None and ss_label is None: 311 if t_label is None and ss_label is None:
270 # Nothing 312 # Nothing
271 continue 313 continue
272 314
273 if (ss_label is None) or (ss_label != t_label): 315 if (t_label is not None) and \
316 ((ss_label is None) or (ss_label != t_label)):
274 ret['labels'].append('-' + t_label) 317 ret['labels'].append('-' + t_label)
275 318
276 if (t_label is None) or (t_label != ss_label): 319 if (ss_label is not None) and \
320 ((t_label is None) or (t_label != ss_label)):
277 ret['labels'].append(ss_label) 321 ret['labels'].append(ss_label)
278 return ret 322 return ret
279 323
280 def tracker_issue_has_changed(self, t_issue, ss_issue): 324 def tracker_issue_has_changed(self, t_issue, ss_issue):
281 """Returns True iff ss_issue indicates changes in t_issue that need to be 325 """Returns True iff ss_issue indicates changes in t_issue that need to be
282 committed up to the Issue Tracker.""" 326 committed up to the Issue Tracker."""
283 if t_issue is None: 327 if t_issue is None:
284 return True 328 return True
285 potential_commit = \ 329 potential_commit = \
286 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue) 330 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue)
(...skipping 28 matching lines...) Expand all
315 ret.append(commit) 359 ret.append(commit)
316 return ret 360 return ret
317 361
318 def fetch_issues(self): 362 def fetch_issues(self):
319 """Logs into Docs/Tracker, and fetches spreadsheet and tracker issues""" 363 """Logs into Docs/Tracker, and fetches spreadsheet and tracker issues"""
320 print 'Logging into Docs...' 364 print 'Logging into Docs...'
321 self.spreadsheet_login() 365 self.spreadsheet_login()
322 print 'Logging into Tracker...' 366 print 'Logging into Tracker...'
323 self.tracker_login() 367 self.tracker_login()
324 368
325 print 'Fetching tracker issues...'
326 t_issues = self.fetch_tracker_issues()
327 self.debug('Tracker issues: %s' % t_issues)
328 print 'Fetching spreadsheet issues...' 369 print 'Fetching spreadsheet issues...'
329 ss_issues = self.fetch_spreadsheet_issues() 370 ss_issues = self.fetch_spreadsheet_issues()
330 self.debug('Spreadsheet issues: %s' % ss_issues) 371 self.debug('Spreadsheet issues: %s' % ss_issues)
372 print 'Fetching tracker issues...'
373 t_issues = self.fetch_tracker_issues(ss_issues)
374 self.debug('Tracker issues: %s' % t_issues)
331 return (t_issues, ss_issues) 375 return (t_issues, ss_issues)
332 376
333 def spreadsheet_to_tracker(self): 377 def spreadsheet_to_tracker(self):
334 """High-level function to manage migrating data from the spreadsheet 378 """High-level function to manage migrating data from the spreadsheet
335 to Tracker.""" 379 to Tracker."""
336 (t_issues, ss_issues) = self.fetch_issues() 380 (t_issues, ss_issues) = self.fetch_issues()
337 print 'Calculating deltas...' 381 print 'Calculating deltas...'
338 commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues) 382 commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues)
339 self.debug('got commits: %s' % commits) 383 self.debug('got commits: %s' % commits)
340 if not commits: 384 if not commits:
341 print 'No deltas. Done.' 385 print 'No deltas. Done.'
342 return 386 return
343 387
344 for commit in commits: 388 for commit in commits:
345 dic = commit['dict'] 389 dic = commit['dict']
346 labels = dic.get('labels') 390 labels = dic.get('labels')
347 owner = dic.get('owner') 391 owner = dic.get('owner')
348 status = dic.get('status') 392 status = dic.get('status')
349 393
350 if commit['type'] == 'append': 394 if commit['type'] == 'append':
351 print 'Creating new tracker issue...' 395 print 'Creating new tracker issue...'
396 if self.pretend:
397 print '(Skipping because --pretend is set)'
398 continue
352 created = self.it_client.add_issue(self.tracker_project, 399 created = self.it_client.add_issue(self.tracker_project,
353 dic['title'], 400 dic['title'],
354 self.tracker_message, 401 self.tracker_message,
355 self.tracker_user, 402 self.tracker_user,
356 labels=labels, 403 labels=labels,
357 owner=owner, 404 owner=owner,
358 status=status) 405 status=status)
359 issue_id = created.id.text.split('/')[-1] 406 issue_id = created.id.text.split('/')[-1]
360 print 'Created issue with id:', issue_id 407 print 'Created issue with id:', issue_id
361 print 'Write id back to spreadsheet row...' 408 print 'Write id back to spreadsheet row...'
362 raw_entry = commit['__ss_issue']['__raw_entry'] 409 raw_entry = commit['__ss_issue']['__raw_entry']
363 ss_issue = commit['__ss_issue'] 410 ss_issue = commit['__ss_issue']
364 del ss_issue['__raw_entry'] 411 del ss_issue['__raw_entry']
365 ss_issue.update({'id': issue_id}) 412 ss_issue.update({'id': issue_id})
366 self.gd_client.UpdateRow(raw_entry, ss_issue) 413 self.gd_client.UpdateRow(raw_entry, ss_issue)
367 print 'Done.' 414 print 'Done.'
368 else: 415 else:
369 print 'Updating issue with id:', dic['id'] 416 print 'Updating issue with id:', dic['id']
417 if self.pretend:
418 print '(Skipping because --pretend is set)'
419 continue
370 self.it_client.update_issue(self.tracker_project, 420 self.it_client.update_issue(self.tracker_project,
371 dic['id'], 421 dic['id'],
372 self.tracker_user, 422 self.tracker_user,
373 comment=self.tracker_message, 423 comment=self.tracker_message,
374 status=status, 424 status=status,
375 owner=owner, 425 owner=owner,
376 labels=labels) 426 labels=labels)
377 print 'Done.' 427 print 'Done.'
378 428
379 def spreadsheet_issue_for_id(self, issues, id_): 429 def spreadsheet_issue_for_id(self, issues, id_):
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
458 ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues) 508 ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues)
459 self.debug('commits: %s' % ss_commits) 509 self.debug('commits: %s' % ss_commits)
460 if not ss_commits: 510 if not ss_commits:
461 print 'Nothing to commit.' 511 print 'Nothing to commit.'
462 return 512 return
463 print 'Committing...' 513 print 'Committing...'
464 for commit in ss_commits: 514 for commit in ss_commits:
465 self.debug('Operating on commit: %s' % commit) 515 self.debug('Operating on commit: %s' % commit)
466 if commit['type'] == 'append': 516 if commit['type'] == 'append':
467 print 'Appending new row...' 517 print 'Appending new row...'
468 self.gd_client.InsertRow(commit['new_row'], self.ss_key, self.ss_ws_key) 518 if not self.pretend:
519 self.gd_client.InsertRow(commit['new_row'],
520 self.ss_key, self.ss_ws_key)
521 else:
522 print '(Skipped because --pretend set)'
469 if commit['type'] == 'update': 523 if commit['type'] == 'update':
470 print 'Updating row...' 524 print 'Updating row...'
471 self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict']) 525 if not self.pretend:
526 self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict'])
527 else:
528 print '(Skipped because --pretend set)'
472 print 'Done.' 529 print 'Done.'
473 530
474 def main(): 531 def main():
475 class PureEpilogOptionParser(OptionParser): 532 class PureEpilogOptionParser(OptionParser):
476 def format_epilog(self, formatter): 533 def format_epilog(self, formatter):
477 return self.epilog 534 return self.epilog
478 535
479 parser = PureEpilogOptionParser() 536 parser = PureEpilogOptionParser()
480 parser.add_option('-a', '--action', dest='action', metavar='ACTION', 537 parser.add_option('-a', '--action', dest='action', metavar='ACTION',
481 help='Action to perform') 538 help='Action to perform')
482 parser.add_option('-d', '--debug', action='store_true', dest='debug', 539 parser.add_option('-d', '--debug', action='store_true', dest='debug',
483 default=False, help='Print debug output.') 540 default=False, help='Print debug output.')
484 parser.add_option('-m', '--message', dest='message', metavar='TEXT', 541 parser.add_option('-m', '--message', dest='message', metavar='TEXT',
485 help='Log message when updating Tracker issues') 542 help='Log message when updating Tracker issues')
543 parser.add_option('-p', '--pretend', action='store_true', dest='pretend',
544 default=False, help="Don't commit anything.")
486 parser.add_option('--ss_key', dest='ss_key', metavar='KEY', 545 parser.add_option('--ss_key', dest='ss_key', metavar='KEY',
487 help='Spreadsheets key (find with browse action)') 546 help='Spreadsheets key (find with browse action)')
488 parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY', 547 parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY',
489 help='Spreadsheets worksheet key (find with browse action)') 548 help='Spreadsheets worksheet key (find with browse action)')
490 parser.add_option('--tracker_project', dest='tracker_project', 549 parser.add_option('--tracker_project', dest='tracker_project',
491 metavar='PROJECT', 550 metavar='PROJECT',
492 help='Tracker project (default: chromium-os)', 551 help='Tracker project (default: chromium-os)',
493 default='chromium-os') 552 default='chromium-os')
494 parser.epilog = """Actions: 553 parser.epilog = """Actions:
495 browse -- browse spreadsheets to find spreadsheet and worksheet keys. 554 browse -- browse spreadsheets to find spreadsheet and worksheet keys.
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
539 a command-line flag. 598 a command-line flag.
540 - When creating a new issue on tracker, the owner field isn't set. I (adlr) 599 - When creating a new issue on tracker, the owner field isn't set. I (adlr)
541 am not sure why. Workaround: If you rerun this script, tho, it will detect 600 am not sure why. Workaround: If you rerun this script, tho, it will detect
542 a delta and update the tracker issue with the owner, which seems to succeed. 601 a delta and update the tracker issue with the owner, which seems to succeed.
543 """ 602 """
544 603
545 (options, args) = parser.parse_args() 604 (options, args) = parser.parse_args()
546 605
547 merger = Merger(options.ss_key, options.ss_ws_key, 606 merger = Merger(options.ss_key, options.ss_ws_key,
548 options.message, options.tracker_project, 607 options.message, options.tracker_project,
549 options.debug) 608 options.debug, options.pretend)
550 if options.action == 'browse': 609 if options.action == 'browse':
551 merger.browse() 610 merger.browse()
552 elif options.action == 'ss_to_t': 611 elif options.action == 'ss_to_t':
553 if not options.message: 612 if not options.message:
554 print 'Error: when updating tracker, -m MESSAGE required.' 613 print 'Error: when updating tracker, -m MESSAGE required.'
555 return 614 return
556 merger.spreadsheet_to_tracker() 615 merger.spreadsheet_to_tracker()
557 elif options.action == 't_to_ss': 616 elif options.action == 't_to_ss':
558 merger.tracker_to_spreadsheet() 617 merger.tracker_to_spreadsheet()
559 else: 618 else:
560 raise Exception('Unknown action requested.') 619 raise Exception('Unknown action requested.')
561 620
562 if __name__ == '__main__': 621 if __name__ == '__main__':
563 main() 622 main()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698