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 |