| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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() |
| OLD | NEW |