| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 | |
| 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 | |
| 5 # found in the LICENSE file. | |
| 6 | |
| 7 # For Spreadsheets: | |
| 8 try: | |
| 9 from xml.etree import ElementTree | |
| 10 except ImportError: | |
| 11 from elementtree import ElementTree | |
| 12 import gdata.spreadsheet.service | |
| 13 import gdata.service | |
| 14 import atom.service | |
| 15 import gdata.spreadsheet | |
| 16 import atom | |
| 17 | |
| 18 # For Issue Tracker: | |
| 19 import gdata.projecthosting.client | |
| 20 import gdata.projecthosting.data | |
| 21 import gdata.gauth | |
| 22 import gdata.client | |
| 23 import gdata.data | |
| 24 import atom.http_core | |
| 25 import atom.core | |
| 26 | |
| 27 # For this script: | |
| 28 import getpass | |
| 29 from optparse import OptionParser | |
| 30 import pickle | |
| 31 from sets import Set | |
| 32 | |
| 33 # Settings | |
| 34 credentials_store = 'creds.dat' | |
| 35 | |
| 36 class Merger(object): | |
| 37 def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project, | |
| 38 debug, pretend): | |
| 39 self.ss_key = ss_key | |
| 40 self.ss_ws_key = ss_ws_key | |
| 41 self.tracker_message = tracker_message | |
| 42 self.tracker_project = tracker_project | |
| 43 self.debug_enabled = debug | |
| 44 self.pretend = pretend | |
| 45 self.user_agent = 'adlr-tracker-spreadsheet-merger' | |
| 46 self.it_keys = ['id', 'owner', 'status', 'title'] | |
| 47 | |
| 48 def debug(self, message): | |
| 49 """Prints message if debug mode is set.""" | |
| 50 if self.debug_enabled: | |
| 51 print message | |
| 52 | |
| 53 def print_feed(self, feed): | |
| 54 'Handy for debugging' | |
| 55 for i, entry in enumerate(feed.entry): | |
| 56 print 'id:', entry.id | |
| 57 if isinstance(feed, gdata.spreadsheet.SpreadsheetsCellsFeed): | |
| 58 print '%s %s\n' % (entry.title.text, entry.content.text) | |
| 59 elif isinstance(feed, gdata.spreadsheet.SpreadsheetsListFeed): | |
| 60 print '%s %s %s' % (i, entry.title.text, entry.content.text) | |
| 61 # Print this row's value for each column (the custom dictionary is | |
| 62 # built using the gsx: elements in the entry.) | |
| 63 print 'Contents:' | |
| 64 for key in entry.custom: | |
| 65 print ' %s: %s' % (key, entry.custom[key].text) | |
| 66 print '\n', | |
| 67 else: | |
| 68 print '%s %s\n' % (i, entry.title.text) | |
| 69 | |
| 70 | |
| 71 def tracker_login(self): | |
| 72 """Logs user into Tracker, using cached credentials if possible. | |
| 73 Saves credentials after login.""" | |
| 74 self.it_client = gdata.projecthosting.client.ProjectHostingClient() | |
| 75 self.it_client.source = self.user_agent | |
| 76 | |
| 77 self.load_creds() | |
| 78 | |
| 79 if self.tracker_token and self.tracker_user: | |
| 80 print 'Using existing credential for tracker login' | |
| 81 self.it_client.auth_token = self.tracker_token | |
| 82 else: | |
| 83 self.tracker_user = raw_input('Issue Tracker Login:') | |
| 84 password = getpass.getpass('Password:') | |
| 85 self.it_client.ClientLogin(self.tracker_user, password, | |
| 86 source=self.user_agent, service='code', | |
| 87 account_type='GOOGLE') | |
| 88 self.tracker_token = self.it_client.auth_token | |
| 89 self.store_creds() | |
| 90 | |
| 91 def spreadsheet_login(self): | |
| 92 """Logs user into Google Spreadsheets, using cached credentials if possible. | |
| 93 Saves credentials after login.""" | |
| 94 self.gd_client = gdata.spreadsheet.service.SpreadsheetsService() | |
| 95 self.gd_client.source = self.user_agent | |
| 96 | |
| 97 self.load_creds() | |
| 98 if self.docs_token: | |
| 99 print 'Using existing credential for docs login' | |
| 100 self.gd_client.SetClientLoginToken(self.docs_token) | |
| 101 else: | |
| 102 self.gd_client.email = raw_input('Google Docs Login:') | |
| 103 self.gd_client.password = getpass.getpass('Password:') | |
| 104 self.gd_client.ProgrammaticLogin() | |
| 105 self.docs_token = self.gd_client.GetClientLoginToken() | |
| 106 self.store_creds() | |
| 107 | |
| 108 def fetch_spreadsheet_issues(self): | |
| 109 """Fetches all issues from the user-specified spreadsheet. Returns | |
| 110 them as an array or dictionaries.""" | |
| 111 feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key) | |
| 112 issues = [] | |
| 113 for entry in feed.entry: | |
| 114 issue = {} | |
| 115 for key in entry.custom: | |
| 116 issue[key] = entry.custom[key].text | |
| 117 issue['__raw_entry'] = entry | |
| 118 issues.append(issue) | |
| 119 return issues | |
| 120 | |
| 121 def ids_for_spreadsheet_issues(self, ss_issues): | |
| 122 """Returns a Set of strings, each string an id from ss_issues""" | |
| 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""" | |
| 150 issues = [] | |
| 151 got_results = True | |
| 152 index = 1 | |
| 153 while got_results: | |
| 154 query = gdata.projecthosting.client.Query(label='Area-Installer', | |
| 155 max_results=50, | |
| 156 start_index=index) | |
| 157 feed = self.it_client.get_issues('chromium-os', query=query) | |
| 158 if not feed.entry: | |
| 159 got_results = False | |
| 160 index = index + len(feed.entry) | |
| 161 issues.extend(self.tracker_issues_for_query_feed(feed)) | |
| 162 # Now, remove issues that are open or in ss_issues. | |
| 163 ss_ids = self.ids_for_spreadsheet_issues(ss_issues) | |
| 164 open_statuses = ['Unconfirmed', 'Untriaged', 'Available', 'Assigned', | |
| 165 'Started', 'Upstream'] | |
| 166 new_issues = [] | |
| 167 for issue in issues: | |
| 168 if issue['status'] in open_statuses or issue['id'] in ss_ids: | |
| 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 | |
| 185 return issues | |
| 186 | |
| 187 def store_creds(self): | |
| 188 """Stores login credentials to disk.""" | |
| 189 obj = {} | |
| 190 if self.docs_token: | |
| 191 obj['docs_token'] = self.docs_token | |
| 192 if self.tracker_token: | |
| 193 obj['tracker_token'] = self.tracker_token | |
| 194 if self.tracker_user: | |
| 195 obj['tracker_user'] = self.tracker_user | |
| 196 try: | |
| 197 f = open(credentials_store, 'w') | |
| 198 pickle.dump(obj, f) | |
| 199 f.close() | |
| 200 except IOError: | |
| 201 print 'Unable to store credentials' | |
| 202 | |
| 203 def load_creds(self): | |
| 204 """Loads login credentials from disk.""" | |
| 205 self.docs_token = None | |
| 206 self.tracker_token = None | |
| 207 self.tracker_user = None | |
| 208 try: | |
| 209 f = open(credentials_store, 'r') | |
| 210 obj = pickle.load(f) | |
| 211 f.close() | |
| 212 if obj.has_key('docs_token'): | |
| 213 self.docs_token = obj['docs_token'] | |
| 214 if obj.has_key('tracker_token'): | |
| 215 self.tracker_token = obj['tracker_token'] | |
| 216 if obj.has_key('tracker_user'): | |
| 217 self.tracker_user = obj['tracker_user'] | |
| 218 except IOError: | |
| 219 print 'Unable to load credentials' | |
| 220 | |
| 221 def browse(self): | |
| 222 """Browses Spreadsheets to help the user find the spreadsheet and | |
| 223 worksheet keys""" | |
| 224 print 'Browsing spreadsheets...' | |
| 225 | |
| 226 if self.ss_key and self.ss_ws_key: | |
| 227 print 'You already passed in --ss_key and --ss_ws_key. No need to browse.' | |
| 228 return | |
| 229 | |
| 230 print 'Logging in...' | |
| 231 self.spreadsheet_login() | |
| 232 | |
| 233 if not self.ss_key: | |
| 234 print 'Fetching spreadsheets...' | |
| 235 feed = self.gd_client.GetSpreadsheetsFeed() | |
| 236 print '' | |
| 237 print 'Spreadsheet key - Title' | |
| 238 for entry in feed.entry: | |
| 239 key = entry.id.text.split('/')[-1] | |
| 240 title = entry.title.text | |
| 241 print '"%s" - "%s"' % (key, title) | |
| 242 print '' | |
| 243 print 'Done. Rerun with --ss_key=KEY to browse a list of worksheet keys.' | |
| 244 else: | |
| 245 print 'Fetching worksheets for spreadsheet', self.ss_key | |
| 246 feed = self.gd_client.GetWorksheetsFeed(self.ss_key) | |
| 247 for entry in feed.entry: | |
| 248 key = entry.id.text.split('/')[-1] | |
| 249 title = entry.title.text | |
| 250 print '' | |
| 251 print 'Worksheet key - Title' | |
| 252 print '"%s" - "%s"' % (key, title) | |
| 253 print '' | |
| 254 print 'Done. You now have keys for --ss_key and --ss_ws_key.' | |
| 255 | |
| 256 def tracker_issue_for_id(self, issues, id_): | |
| 257 """Returns the element of issues which has id_ for the key 'id'""" | |
| 258 for issue in issues: | |
| 259 if issue['id'] == id_: | |
| 260 return issue | |
| 261 return None | |
| 262 | |
| 263 def spreadsheet_issue_to_tracker_dict(self, ss_issue): | |
| 264 """Converts a spreadsheet issue to the dict format that is used to | |
| 265 represent a tracker issue.""" | |
| 266 ret = {} | |
| 267 ret['project'] = self.tracker_project | |
| 268 ret['title'] = ss_issue['title'] | |
| 269 ret['summary'] = self.tracker_message | |
| 270 ret['owner'] = ss_issue['owner'] | |
| 271 if ss_issue.get('status') is not None: | |
| 272 ret['status'] = ss_issue['status'] | |
| 273 ret['labels'] = [] | |
| 274 for (key, value) in ss_issue.items(): | |
| 275 if key.endswith('-') and (value is not None): | |
| 276 ret['labels'].append(key.title() + value) | |
| 277 return ret | |
| 278 | |
| 279 def label_from_prefix(self, prefix, corpus): | |
| 280 """Given a corpus (array of lable strings), return the first label | |
| 281 that begins with the specified prefix.""" | |
| 282 for label in corpus: | |
| 283 if label.startswith(prefix): | |
| 284 return label | |
| 285 return None | |
| 286 | |
| 287 def update_spreadsheet_issue_to_tracker_dict(self, ss_issue, t_issue): | |
| 288 """Updates a given tracker issue with data from the spreadsheet issue.""" | |
| 289 ret = {} | |
| 290 ret['title'] = ss_issue['title'] | |
| 291 ret['id'] = ss_issue['id'] | |
| 292 ret['summary'] = self.tracker_message | |
| 293 if ss_issue['status'] != t_issue['status']: | |
| 294 ret['status'] = ss_issue['status'] | |
| 295 | |
| 296 if ss_issue.get('owner'): | |
| 297 if (not t_issue.has_key('owner')) or \ | |
| 298 (ss_issue['owner'] != t_issue['owner']): | |
| 299 ret['owner'] = ss_issue['owner'] | |
| 300 # labels | |
| 301 ret['labels'] = [] | |
| 302 for (key, value) in ss_issue.items(): | |
| 303 caps_key = key.title() | |
| 304 if not caps_key.endswith('-'): | |
| 305 continue | |
| 306 ss_label = None | |
| 307 if value: | |
| 308 ss_label = caps_key + value.title() | |
| 309 t_label = self.label_from_prefix(caps_key, t_issue['labels']) | |
| 310 | |
| 311 if t_label is None and ss_label is None: | |
| 312 # Nothing | |
| 313 continue | |
| 314 | |
| 315 if (t_label is not None) and \ | |
| 316 ((ss_label is None) or (ss_label != t_label)): | |
| 317 ret['labels'].append('-' + t_label) | |
| 318 | |
| 319 if (ss_label is not None) and \ | |
| 320 ((t_label is None) or (t_label != ss_label)): | |
| 321 ret['labels'].append(ss_label) | |
| 322 return ret | |
| 323 | |
| 324 def tracker_issue_has_changed(self, t_issue, ss_issue): | |
| 325 """Returns True iff ss_issue indicates changes in t_issue that need to be | |
| 326 committed up to the Issue Tracker.""" | |
| 327 if t_issue is None: | |
| 328 return True | |
| 329 potential_commit = \ | |
| 330 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue) | |
| 331 | |
| 332 if potential_commit.has_key('status') or \ | |
| 333 potential_commit.has_key('owner') or \ | |
| 334 (len(potential_commit['labels']) > 0): | |
| 335 return True | |
| 336 if potential_commit['title'] != t_issue['title']: | |
| 337 return True | |
| 338 return False | |
| 339 | |
| 340 def spreadsheet_to_tracker_commits(self, ss_issues, t_issues): | |
| 341 """Given the current state of all spreadsheet issues and tracker issues, | |
| 342 returns a list of all commits that need to go to tracker to get it in | |
| 343 line with the spreadsheet.""" | |
| 344 ret = [] | |
| 345 for ss_issue in ss_issues: | |
| 346 t_issue = self.tracker_issue_for_id(t_issues, ss_issue['id']) | |
| 347 commit = {} | |
| 348 # TODO see if an update is needed at all | |
| 349 if t_issue is None: | |
| 350 commit['type'] = 'append' | |
| 351 commit['dict'] = self.spreadsheet_issue_to_tracker_dict(ss_issue) | |
| 352 commit['__ss_issue'] = ss_issue | |
| 353 else: | |
| 354 if not self.tracker_issue_has_changed(t_issue, ss_issue): | |
| 355 continue | |
| 356 commit['type'] = 'update' | |
| 357 commit['dict'] = \ | |
| 358 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue) | |
| 359 ret.append(commit) | |
| 360 return ret | |
| 361 | |
| 362 def fetch_issues(self): | |
| 363 """Logs into Docs/Tracker, and fetches spreadsheet and tracker issues""" | |
| 364 print 'Logging into Docs...' | |
| 365 self.spreadsheet_login() | |
| 366 print 'Logging into Tracker...' | |
| 367 self.tracker_login() | |
| 368 | |
| 369 print 'Fetching spreadsheet issues...' | |
| 370 ss_issues = self.fetch_spreadsheet_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) | |
| 375 return (t_issues, ss_issues) | |
| 376 | |
| 377 def spreadsheet_to_tracker(self): | |
| 378 """High-level function to manage migrating data from the spreadsheet | |
| 379 to Tracker.""" | |
| 380 (t_issues, ss_issues) = self.fetch_issues() | |
| 381 print 'Calculating deltas...' | |
| 382 commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues) | |
| 383 self.debug('got commits: %s' % commits) | |
| 384 if not commits: | |
| 385 print 'No deltas. Done.' | |
| 386 return | |
| 387 | |
| 388 for commit in commits: | |
| 389 dic = commit['dict'] | |
| 390 labels = dic.get('labels') | |
| 391 owner = dic.get('owner') | |
| 392 status = dic.get('status') | |
| 393 | |
| 394 if commit['type'] == 'append': | |
| 395 print 'Creating new tracker issue...' | |
| 396 if self.pretend: | |
| 397 print '(Skipping because --pretend is set)' | |
| 398 continue | |
| 399 created = self.it_client.add_issue(self.tracker_project, | |
| 400 dic['title'], | |
| 401 self.tracker_message, | |
| 402 self.tracker_user, | |
| 403 labels=labels, | |
| 404 owner=owner, | |
| 405 status=status) | |
| 406 issue_id = created.id.text.split('/')[-1] | |
| 407 print 'Created issue with id:', issue_id | |
| 408 print 'Write id back to spreadsheet row...' | |
| 409 raw_entry = commit['__ss_issue']['__raw_entry'] | |
| 410 ss_issue = commit['__ss_issue'] | |
| 411 del ss_issue['__raw_entry'] | |
| 412 ss_issue.update({'id': issue_id}) | |
| 413 self.gd_client.UpdateRow(raw_entry, ss_issue) | |
| 414 print 'Done.' | |
| 415 else: | |
| 416 print 'Updating issue with id:', dic['id'] | |
| 417 if self.pretend: | |
| 418 print '(Skipping because --pretend is set)' | |
| 419 continue | |
| 420 self.it_client.update_issue(self.tracker_project, | |
| 421 dic['id'], | |
| 422 self.tracker_user, | |
| 423 comment=self.tracker_message, | |
| 424 status=status, | |
| 425 owner=owner, | |
| 426 labels=labels) | |
| 427 print 'Done.' | |
| 428 | |
| 429 def spreadsheet_issue_for_id(self, issues, id_): | |
| 430 """Given the array of spreadsheet issues, return the first one that | |
| 431 has id_ for the key 'id'.""" | |
| 432 for issue in issues: | |
| 433 if issue['id'] == id_: | |
| 434 return issue | |
| 435 return None | |
| 436 | |
| 437 def value_for_key_in_labels(self, label_array, prefix): | |
| 438 """Given an array of labels and a prefix, return the non-prefix part | |
| 439 of the first label that has that prefix. E.g. if label_array is | |
| 440 ["Mstone-R7", "Area-Installer"] and prefix is "Area-", returns | |
| 441 "Installer".""" | |
| 442 for label in label_array: | |
| 443 if label.startswith(prefix): | |
| 444 return label[len(prefix):] | |
| 445 return None | |
| 446 | |
| 447 def tracker_issue_to_spreadsheet_issue(self, t_issue, ss_keys): | |
| 448 """Converts a tracker issue to the format used by spreadsheet, given | |
| 449 the row headings ss_keys.""" | |
| 450 new_row = {} | |
| 451 for key in ss_keys: | |
| 452 if key.endswith('-'): | |
| 453 # label | |
| 454 new_row[key] = self.value_for_key_in_labels(t_issue['labels'], | |
| 455 key.title()) | |
| 456 # Special cases | |
| 457 if key in self.it_keys and key in t_issue: | |
| 458 new_row[key] = t_issue[key] | |
| 459 return new_row | |
| 460 | |
| 461 def spreadsheet_row_needs_update(self, ss_issue, t_issue): | |
| 462 """Returns True iff the spreadsheet issue passed in needs to be updated | |
| 463 to match data in the tracker issue.""" | |
| 464 new_ss_issue = self.tracker_issue_to_spreadsheet_issue(t_issue, | |
| 465 ss_issue.keys()) | |
| 466 for key in new_ss_issue.keys(): | |
| 467 if not ss_issue.has_key(key): | |
| 468 continue | |
| 469 if new_ss_issue[key] != ss_issue[key]: | |
| 470 return True | |
| 471 return False | |
| 472 | |
| 473 def tracker_to_spreadsheet_commits(self, t_issues, ss_issues): | |
| 474 """Given the current set of spreadsheet and tracker issues, computes | |
| 475 commits needed to go to Spreadsheets to get the spreadsheet in line | |
| 476 with what's in Tracker.""" | |
| 477 ret = [] | |
| 478 keys = ss_issues[0].keys() | |
| 479 for t_issue in t_issues: | |
| 480 commit = {} | |
| 481 ss_issue = self.spreadsheet_issue_for_id(ss_issues, t_issue['id']) | |
| 482 if ss_issue is None: | |
| 483 # New issue | |
| 484 commit['new_row'] = self.tracker_issue_to_spreadsheet_issue(t_issue, | |
| 485 keys) | |
| 486 commit['type'] = 'append' | |
| 487 elif self.spreadsheet_row_needs_update(ss_issue, t_issue): | |
| 488 commit['__raw_entry'] = ss_issue['__raw_entry'] | |
| 489 del ss_issue['__raw_entry'] | |
| 490 ss_issue.update(self.tracker_issue_to_spreadsheet_issue(t_issue, keys)) | |
| 491 commit['dict'] = ss_issue | |
| 492 commit['type'] = 'update' | |
| 493 else: | |
| 494 continue | |
| 495 ret.append(commit) | |
| 496 return ret | |
| 497 | |
| 498 def tracker_to_spreadsheet(self): | |
| 499 """High-level function to migrate data from Tracker to the spreadsheet.""" | |
| 500 (t_issues, ss_issues) = self.fetch_issues() | |
| 501 if len(ss_issues) == 0: | |
| 502 raise Exception('Error: must have at least one non-header row in '\ | |
| 503 'spreadsheet') | |
| 504 return | |
| 505 ss_keys = ss_issues[0].keys() | |
| 506 | |
| 507 print 'Calculating deltas...' | |
| 508 ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues) | |
| 509 self.debug('commits: %s' % ss_commits) | |
| 510 if not ss_commits: | |
| 511 print 'Nothing to commit.' | |
| 512 return | |
| 513 print 'Committing...' | |
| 514 for commit in ss_commits: | |
| 515 self.debug('Operating on commit: %s' % commit) | |
| 516 if commit['type'] == 'append': | |
| 517 print 'Appending new row...' | |
| 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)' | |
| 523 if commit['type'] == 'update': | |
| 524 print 'Updating row...' | |
| 525 if not self.pretend: | |
| 526 self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict']) | |
| 527 else: | |
| 528 print '(Skipped because --pretend set)' | |
| 529 print 'Done.' | |
| 530 | |
| 531 def main(): | |
| 532 class PureEpilogOptionParser(OptionParser): | |
| 533 def format_epilog(self, formatter): | |
| 534 return self.epilog | |
| 535 | |
| 536 parser = PureEpilogOptionParser() | |
| 537 parser.add_option('-a', '--action', dest='action', metavar='ACTION', | |
| 538 help='Action to perform') | |
| 539 parser.add_option('-d', '--debug', action='store_true', dest='debug', | |
| 540 default=False, help='Print debug output.') | |
| 541 parser.add_option('-m', '--message', dest='message', metavar='TEXT', | |
| 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.") | |
| 545 parser.add_option('--ss_key', dest='ss_key', metavar='KEY', | |
| 546 help='Spreadsheets key (find with browse action)') | |
| 547 parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY', | |
| 548 help='Spreadsheets worksheet key (find with browse action)') | |
| 549 parser.add_option('--tracker_project', dest='tracker_project', | |
| 550 metavar='PROJECT', | |
| 551 help='Tracker project (default: chromium-os)', | |
| 552 default='chromium-os') | |
| 553 parser.epilog = """Actions: | |
| 554 browse -- browse spreadsheets to find spreadsheet and worksheet keys. | |
| 555 ss_to_t -- for each entry in spreadsheet, apply its values to tracker. | |
| 556 If no ID is in the spreadsheet row, a new tracker item is created | |
| 557 and the spreadsheet is updated. | |
| 558 t_to_ss -- for each tracker entry, apply it or add it to the spreadsheet. | |
| 559 | |
| 560 | |
| 561 This script can be used to migrate Issue Tracker issues between Issue Tracker | |
| 562 and Google Spreadsheets. The spreadsheet should have certain columns in any | |
| 563 order: Id, Owner, Title, Status. The spreadsheet may have any label of the | |
| 564 form 'Key-'. For those labels that end in '-', this script assumes the cell | |
| 565 value and the header form a label that should be applied to the issue. E.g. | |
| 566 if the spredsheet has a column named 'Mstone-' and a cell under it called | |
| 567 'R8' that corresponds to the label 'Mstone-R8' in Issue Tracker. | |
| 568 | |
| 569 To migrate data, you must choose on each invocation of this script if you | |
| 570 wish to migrate data from Issue Tracker to a spreadsheet of vice-versa. | |
| 571 | |
| 572 When migrating from Tracker, all found issues based on the query | |
| 573 (which is currently hard-coded to "label=Area-Installer") will be inserted | |
| 574 into the spreadsheet (overwritng existing cells if a row with matching ID | |
| 575 is found). Custom columns in the spreadsheet won't be overwritten, so if | |
| 576 the spreadsheet contains extra columns about issues (e.g. time estimates) | |
| 577 they will be preserved. | |
| 578 | |
| 579 When migrating from spreadsheet to Tracker, each row in the spreadsheet | |
| 580 is compared to existing tracker issues that match the query | |
| 581 (which is currently hard-coded to "label=Area-Installer"). If the | |
| 582 spreadsheet row has no Id, a new Issue Tracker issue is created and the new | |
| 583 Id is written back to the spreadsheet. If an existing tracker issue exists, | |
| 584 it's updated with the data from the spreadsheet if anything has changed. | |
| 585 | |
| 586 Suggested usage: | |
| 587 - Create a spreadsheet with columns Id, Owner, Title, Status, and any label | |
| 588 prefixes as desired. | |
| 589 - Run this script with '-b' to browse your spreadsheet and get the | |
| 590 spreadsheet key. | |
| 591 - Run this script again with '-b' and the spreadsheet key to get the | |
| 592 worksheet key. | |
| 593 - Run this script with "-a t_to_ss" or "-a ss_to_t" to migrate data in either | |
| 594 direction. | |
| 595 | |
| 596 Known issues: | |
| 597 - query is currently hardcoded to label=Area-Installer. That should be | |
| 598 a command-line flag. | |
| 599 - When creating a new issue on tracker, the owner field isn't set. I (adlr) | |
| 600 am not sure why. Workaround: If you rerun this script, tho, it will detect | |
| 601 a delta and update the tracker issue with the owner, which seems to succeed. | |
| 602 """ | |
| 603 | |
| 604 (options, args) = parser.parse_args() | |
| 605 | |
| 606 merger = Merger(options.ss_key, options.ss_ws_key, | |
| 607 options.message, options.tracker_project, | |
| 608 options.debug, options.pretend) | |
| 609 if options.action == 'browse': | |
| 610 merger.browse() | |
| 611 elif options.action == 'ss_to_t': | |
| 612 if not options.message: | |
| 613 print 'Error: when updating tracker, -m MESSAGE required.' | |
| 614 return | |
| 615 merger.spreadsheet_to_tracker() | |
| 616 elif options.action == 't_to_ss': | |
| 617 merger.tracker_to_spreadsheet() | |
| 618 else: | |
| 619 raise Exception('Unknown action requested.') | |
| 620 | |
| 621 if __name__ == '__main__': | |
| 622 main() | |
| OLD | NEW |