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