mirror of
https://github.com/flatcar/scripts.git
synced 2025-08-08 13:36:58 +02:00
Migrate only issues that are open or that are already in the spreadsheet. This way, the spreadsheet doesn't get cluttered w/ closed tasks. Review URL: http://codereview.chromium.org/1734001
623 lines
23 KiB
Python
Executable File
623 lines
23 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
# For Spreadsheets:
|
|
try:
|
|
from xml.etree import ElementTree
|
|
except ImportError:
|
|
from elementtree import ElementTree
|
|
import gdata.spreadsheet.service
|
|
import gdata.service
|
|
import atom.service
|
|
import gdata.spreadsheet
|
|
import atom
|
|
|
|
# For Issue Tracker:
|
|
import gdata.projecthosting.client
|
|
import gdata.projecthosting.data
|
|
import gdata.gauth
|
|
import gdata.client
|
|
import gdata.data
|
|
import atom.http_core
|
|
import atom.core
|
|
|
|
# For this script:
|
|
import getpass
|
|
from optparse import OptionParser
|
|
import pickle
|
|
from sets import Set
|
|
|
|
# Settings
|
|
credentials_store = 'creds.dat'
|
|
|
|
class Merger(object):
|
|
def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project,
|
|
debug, pretend):
|
|
self.ss_key = ss_key
|
|
self.ss_ws_key = ss_ws_key
|
|
self.tracker_message = tracker_message
|
|
self.tracker_project = tracker_project
|
|
self.debug_enabled = debug
|
|
self.pretend = pretend
|
|
self.user_agent = 'adlr-tracker-spreadsheet-merger'
|
|
self.it_keys = ['id', 'owner', 'status', 'title']
|
|
|
|
def debug(self, message):
|
|
"""Prints message if debug mode is set."""
|
|
if self.debug_enabled:
|
|
print message
|
|
|
|
def print_feed(self, feed):
|
|
'Handy for debugging'
|
|
for i, entry in enumerate(feed.entry):
|
|
print 'id:', entry.id
|
|
if isinstance(feed, gdata.spreadsheet.SpreadsheetsCellsFeed):
|
|
print '%s %s\n' % (entry.title.text, entry.content.text)
|
|
elif isinstance(feed, gdata.spreadsheet.SpreadsheetsListFeed):
|
|
print '%s %s %s' % (i, entry.title.text, entry.content.text)
|
|
# Print this row's value for each column (the custom dictionary is
|
|
# built using the gsx: elements in the entry.)
|
|
print 'Contents:'
|
|
for key in entry.custom:
|
|
print ' %s: %s' % (key, entry.custom[key].text)
|
|
print '\n',
|
|
else:
|
|
print '%s %s\n' % (i, entry.title.text)
|
|
|
|
|
|
def tracker_login(self):
|
|
"""Logs user into Tracker, using cached credentials if possible.
|
|
Saves credentials after login."""
|
|
self.it_client = gdata.projecthosting.client.ProjectHostingClient()
|
|
self.it_client.source = self.user_agent
|
|
|
|
self.load_creds()
|
|
|
|
if self.tracker_token and self.tracker_user:
|
|
print 'Using existing credential for tracker login'
|
|
self.it_client.auth_token = self.tracker_token
|
|
else:
|
|
self.tracker_user = raw_input('Issue Tracker Login:')
|
|
password = getpass.getpass('Password:')
|
|
self.it_client.ClientLogin(self.tracker_user, password,
|
|
source=self.user_agent, service='code',
|
|
account_type='GOOGLE')
|
|
self.tracker_token = self.it_client.auth_token
|
|
self.store_creds()
|
|
|
|
def spreadsheet_login(self):
|
|
"""Logs user into Google Spreadsheets, using cached credentials if possible.
|
|
Saves credentials after login."""
|
|
self.gd_client = gdata.spreadsheet.service.SpreadsheetsService()
|
|
self.gd_client.source = self.user_agent
|
|
|
|
self.load_creds()
|
|
if self.docs_token:
|
|
print 'Using existing credential for docs login'
|
|
self.gd_client.SetClientLoginToken(self.docs_token)
|
|
else:
|
|
self.gd_client.email = raw_input('Google Docs Login:')
|
|
self.gd_client.password = getpass.getpass('Password:')
|
|
self.gd_client.ProgrammaticLogin()
|
|
self.docs_token = self.gd_client.GetClientLoginToken()
|
|
self.store_creds()
|
|
|
|
def fetch_spreadsheet_issues(self):
|
|
"""Fetches all issues from the user-specified spreadsheet. Returns
|
|
them as an array or dictionaries."""
|
|
feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key)
|
|
issues = []
|
|
for entry in feed.entry:
|
|
issue = {}
|
|
for key in entry.custom:
|
|
issue[key] = entry.custom[key].text
|
|
issue['__raw_entry'] = entry
|
|
issues.append(issue)
|
|
return issues
|
|
|
|
def ids_for_spreadsheet_issues(self, ss_issues):
|
|
"""Returns a Set of strings, each string an id from ss_issues"""
|
|
ret = Set()
|
|
for ss_issue in ss_issues:
|
|
ret.add(ss_issue['id'])
|
|
return ret
|
|
|
|
def tracker_issues_for_query_feed(self, feed):
|
|
"""Converts a feed object from a query to a list of tracker issue
|
|
dictionaries."""
|
|
issues = []
|
|
for issue in feed.entry:
|
|
issue_dict = {}
|
|
issue_dict['labels'] = [label.text for label in issue.label]
|
|
issue_dict['id'] = issue.id.text.split('/')[-1]
|
|
issue_dict['title'] = issue.title.text
|
|
issue_dict['status'] = issue.status.text
|
|
if issue.owner:
|
|
issue_dict['owner'] = issue.owner.username.text
|
|
issues.append(issue_dict)
|
|
return issues
|
|
|
|
def fetch_tracker_issues(self, ss_issues):
|
|
"""Fetches all relevant issues from traacker and returns them as an array
|
|
of dictionaries. Relevance is:
|
|
- has an ID that's in ss_issues, OR
|
|
- (is Area=Installer AND status is open).
|
|
Open status is one of: Unconfirmed, Untriaged, Available, Assigned,
|
|
Started, Upstream"""
|
|
issues = []
|
|
got_results = True
|
|
index = 1
|
|
while got_results:
|
|
query = gdata.projecthosting.client.Query(label='Area-Installer',
|
|
max_results=50,
|
|
start_index=index)
|
|
feed = self.it_client.get_issues('chromium-os', query=query)
|
|
if not feed.entry:
|
|
got_results = False
|
|
index = index + len(feed.entry)
|
|
issues.extend(self.tracker_issues_for_query_feed(feed))
|
|
# Now, remove issues that are open or in ss_issues.
|
|
ss_ids = self.ids_for_spreadsheet_issues(ss_issues)
|
|
open_statuses = ['Unconfirmed', 'Untriaged', 'Available', 'Assigned',
|
|
'Started', 'Upstream']
|
|
new_issues = []
|
|
for issue in issues:
|
|
if issue['status'] in open_statuses or issue['id'] in ss_ids:
|
|
new_issues.append(issue)
|
|
# Remove id from ss_ids, if it's there
|
|
ss_ids.discard(issue['id'])
|
|
issues = new_issues
|
|
|
|
# Now, for each ss_id that didn't turn up in the query, explicitly add it
|
|
for id_ in ss_ids:
|
|
query = gdata.projecthosting.client.Query(issue_id=id_,
|
|
max_results=50,
|
|
start_index=index)
|
|
feed = self.it_client.get_issues('chromium-os', query=query)
|
|
if not feed.entry:
|
|
print 'No result for id', id_
|
|
continue
|
|
issues.extend(self.tracker_issues_for_query_feed(feed))
|
|
|
|
return issues
|
|
|
|
def store_creds(self):
|
|
"""Stores login credentials to disk."""
|
|
obj = {}
|
|
if self.docs_token:
|
|
obj['docs_token'] = self.docs_token
|
|
if self.tracker_token:
|
|
obj['tracker_token'] = self.tracker_token
|
|
if self.tracker_user:
|
|
obj['tracker_user'] = self.tracker_user
|
|
try:
|
|
f = open(credentials_store, 'w')
|
|
pickle.dump(obj, f)
|
|
f.close()
|
|
except IOError:
|
|
print 'Unable to store credentials'
|
|
|
|
def load_creds(self):
|
|
"""Loads login credentials from disk."""
|
|
self.docs_token = None
|
|
self.tracker_token = None
|
|
self.tracker_user = None
|
|
try:
|
|
f = open(credentials_store, 'r')
|
|
obj = pickle.load(f)
|
|
f.close()
|
|
if obj.has_key('docs_token'):
|
|
self.docs_token = obj['docs_token']
|
|
if obj.has_key('tracker_token'):
|
|
self.tracker_token = obj['tracker_token']
|
|
if obj.has_key('tracker_user'):
|
|
self.tracker_user = obj['tracker_user']
|
|
except IOError:
|
|
print 'Unable to load credentials'
|
|
|
|
def browse(self):
|
|
"""Browses Spreadsheets to help the user find the spreadsheet and
|
|
worksheet keys"""
|
|
print 'Browsing spreadsheets...'
|
|
|
|
if self.ss_key and self.ss_ws_key:
|
|
print 'You already passed in --ss_key and --ss_ws_key. No need to browse.'
|
|
return
|
|
|
|
print 'Logging in...'
|
|
self.spreadsheet_login()
|
|
|
|
if not self.ss_key:
|
|
print 'Fetching spreadsheets...'
|
|
feed = self.gd_client.GetSpreadsheetsFeed()
|
|
print ''
|
|
print 'Spreadsheet key - Title'
|
|
for entry in feed.entry:
|
|
key = entry.id.text.split('/')[-1]
|
|
title = entry.title.text
|
|
print '"%s" - "%s"' % (key, title)
|
|
print ''
|
|
print 'Done. Rerun with --ss_key=KEY to browse a list of worksheet keys.'
|
|
else:
|
|
print 'Fetching worksheets for spreadsheet', self.ss_key
|
|
feed = self.gd_client.GetWorksheetsFeed(self.ss_key)
|
|
for entry in feed.entry:
|
|
key = entry.id.text.split('/')[-1]
|
|
title = entry.title.text
|
|
print ''
|
|
print 'Worksheet key - Title'
|
|
print '"%s" - "%s"' % (key, title)
|
|
print ''
|
|
print 'Done. You now have keys for --ss_key and --ss_ws_key.'
|
|
|
|
def tracker_issue_for_id(self, issues, id_):
|
|
"""Returns the element of issues which has id_ for the key 'id'"""
|
|
for issue in issues:
|
|
if issue['id'] == id_:
|
|
return issue
|
|
return None
|
|
|
|
def spreadsheet_issue_to_tracker_dict(self, ss_issue):
|
|
"""Converts a spreadsheet issue to the dict format that is used to
|
|
represent a tracker issue."""
|
|
ret = {}
|
|
ret['project'] = self.tracker_project
|
|
ret['title'] = ss_issue['title']
|
|
ret['summary'] = self.tracker_message
|
|
ret['owner'] = ss_issue['owner']
|
|
if ss_issue.get('status') is not None:
|
|
ret['status'] = ss_issue['status']
|
|
ret['labels'] = []
|
|
for (key, value) in ss_issue.items():
|
|
if key.endswith('-') and (value is not None):
|
|
ret['labels'].append(key.title() + value)
|
|
return ret
|
|
|
|
def label_from_prefix(self, prefix, corpus):
|
|
"""Given a corpus (array of lable strings), return the first label
|
|
that begins with the specified prefix."""
|
|
for label in corpus:
|
|
if label.startswith(prefix):
|
|
return label
|
|
return None
|
|
|
|
def update_spreadsheet_issue_to_tracker_dict(self, ss_issue, t_issue):
|
|
"""Updates a given tracker issue with data from the spreadsheet issue."""
|
|
ret = {}
|
|
ret['title'] = ss_issue['title']
|
|
ret['id'] = ss_issue['id']
|
|
ret['summary'] = self.tracker_message
|
|
if ss_issue['status'] != t_issue['status']:
|
|
ret['status'] = ss_issue['status']
|
|
|
|
if ss_issue.get('owner'):
|
|
if (not t_issue.has_key('owner')) or \
|
|
(ss_issue['owner'] != t_issue['owner']):
|
|
ret['owner'] = ss_issue['owner']
|
|
# labels
|
|
ret['labels'] = []
|
|
for (key, value) in ss_issue.items():
|
|
caps_key = key.title()
|
|
if not caps_key.endswith('-'):
|
|
continue
|
|
ss_label = None
|
|
if value:
|
|
ss_label = caps_key + value.title()
|
|
t_label = self.label_from_prefix(caps_key, t_issue['labels'])
|
|
|
|
if t_label is None and ss_label is None:
|
|
# Nothing
|
|
continue
|
|
|
|
if (t_label is not None) and \
|
|
((ss_label is None) or (ss_label != t_label)):
|
|
ret['labels'].append('-' + t_label)
|
|
|
|
if (ss_label is not None) and \
|
|
((t_label is None) or (t_label != ss_label)):
|
|
ret['labels'].append(ss_label)
|
|
return ret
|
|
|
|
def tracker_issue_has_changed(self, t_issue, ss_issue):
|
|
"""Returns True iff ss_issue indicates changes in t_issue that need to be
|
|
committed up to the Issue Tracker."""
|
|
if t_issue is None:
|
|
return True
|
|
potential_commit = \
|
|
self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue)
|
|
|
|
if potential_commit.has_key('status') or \
|
|
potential_commit.has_key('owner') or \
|
|
(len(potential_commit['labels']) > 0):
|
|
return True
|
|
if potential_commit['title'] != t_issue['title']:
|
|
return True
|
|
return False
|
|
|
|
def spreadsheet_to_tracker_commits(self, ss_issues, t_issues):
|
|
"""Given the current state of all spreadsheet issues and tracker issues,
|
|
returns a list of all commits that need to go to tracker to get it in
|
|
line with the spreadsheet."""
|
|
ret = []
|
|
for ss_issue in ss_issues:
|
|
t_issue = self.tracker_issue_for_id(t_issues, ss_issue['id'])
|
|
commit = {}
|
|
# TODO see if an update is needed at all
|
|
if t_issue is None:
|
|
commit['type'] = 'append'
|
|
commit['dict'] = self.spreadsheet_issue_to_tracker_dict(ss_issue)
|
|
commit['__ss_issue'] = ss_issue
|
|
else:
|
|
if not self.tracker_issue_has_changed(t_issue, ss_issue):
|
|
continue
|
|
commit['type'] = 'update'
|
|
commit['dict'] = \
|
|
self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue)
|
|
ret.append(commit)
|
|
return ret
|
|
|
|
def fetch_issues(self):
|
|
"""Logs into Docs/Tracker, and fetches spreadsheet and tracker issues"""
|
|
print 'Logging into Docs...'
|
|
self.spreadsheet_login()
|
|
print 'Logging into Tracker...'
|
|
self.tracker_login()
|
|
|
|
print 'Fetching spreadsheet issues...'
|
|
ss_issues = self.fetch_spreadsheet_issues()
|
|
self.debug('Spreadsheet issues: %s' % ss_issues)
|
|
print 'Fetching tracker issues...'
|
|
t_issues = self.fetch_tracker_issues(ss_issues)
|
|
self.debug('Tracker issues: %s' % t_issues)
|
|
return (t_issues, ss_issues)
|
|
|
|
def spreadsheet_to_tracker(self):
|
|
"""High-level function to manage migrating data from the spreadsheet
|
|
to Tracker."""
|
|
(t_issues, ss_issues) = self.fetch_issues()
|
|
print 'Calculating deltas...'
|
|
commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues)
|
|
self.debug('got commits: %s' % commits)
|
|
if not commits:
|
|
print 'No deltas. Done.'
|
|
return
|
|
|
|
for commit in commits:
|
|
dic = commit['dict']
|
|
labels = dic.get('labels')
|
|
owner = dic.get('owner')
|
|
status = dic.get('status')
|
|
|
|
if commit['type'] == 'append':
|
|
print 'Creating new tracker issue...'
|
|
if self.pretend:
|
|
print '(Skipping because --pretend is set)'
|
|
continue
|
|
created = self.it_client.add_issue(self.tracker_project,
|
|
dic['title'],
|
|
self.tracker_message,
|
|
self.tracker_user,
|
|
labels=labels,
|
|
owner=owner,
|
|
status=status)
|
|
issue_id = created.id.text.split('/')[-1]
|
|
print 'Created issue with id:', issue_id
|
|
print 'Write id back to spreadsheet row...'
|
|
raw_entry = commit['__ss_issue']['__raw_entry']
|
|
ss_issue = commit['__ss_issue']
|
|
del ss_issue['__raw_entry']
|
|
ss_issue.update({'id': issue_id})
|
|
self.gd_client.UpdateRow(raw_entry, ss_issue)
|
|
print 'Done.'
|
|
else:
|
|
print 'Updating issue with id:', dic['id']
|
|
if self.pretend:
|
|
print '(Skipping because --pretend is set)'
|
|
continue
|
|
self.it_client.update_issue(self.tracker_project,
|
|
dic['id'],
|
|
self.tracker_user,
|
|
comment=self.tracker_message,
|
|
status=status,
|
|
owner=owner,
|
|
labels=labels)
|
|
print 'Done.'
|
|
|
|
def spreadsheet_issue_for_id(self, issues, id_):
|
|
"""Given the array of spreadsheet issues, return the first one that
|
|
has id_ for the key 'id'."""
|
|
for issue in issues:
|
|
if issue['id'] == id_:
|
|
return issue
|
|
return None
|
|
|
|
def value_for_key_in_labels(self, label_array, prefix):
|
|
"""Given an array of labels and a prefix, return the non-prefix part
|
|
of the first label that has that prefix. E.g. if label_array is
|
|
["Mstone-R7", "Area-Installer"] and prefix is "Area-", returns
|
|
"Installer"."""
|
|
for label in label_array:
|
|
if label.startswith(prefix):
|
|
return label[len(prefix):]
|
|
return None
|
|
|
|
def tracker_issue_to_spreadsheet_issue(self, t_issue, ss_keys):
|
|
"""Converts a tracker issue to the format used by spreadsheet, given
|
|
the row headings ss_keys."""
|
|
new_row = {}
|
|
for key in ss_keys:
|
|
if key.endswith('-'):
|
|
# label
|
|
new_row[key] = self.value_for_key_in_labels(t_issue['labels'],
|
|
key.title())
|
|
# Special cases
|
|
if key in self.it_keys and key in t_issue:
|
|
new_row[key] = t_issue[key]
|
|
return new_row
|
|
|
|
def spreadsheet_row_needs_update(self, ss_issue, t_issue):
|
|
"""Returns True iff the spreadsheet issue passed in needs to be updated
|
|
to match data in the tracker issue."""
|
|
new_ss_issue = self.tracker_issue_to_spreadsheet_issue(t_issue,
|
|
ss_issue.keys())
|
|
for key in new_ss_issue.keys():
|
|
if not ss_issue.has_key(key):
|
|
continue
|
|
if new_ss_issue[key] != ss_issue[key]:
|
|
return True
|
|
return False
|
|
|
|
def tracker_to_spreadsheet_commits(self, t_issues, ss_issues):
|
|
"""Given the current set of spreadsheet and tracker issues, computes
|
|
commits needed to go to Spreadsheets to get the spreadsheet in line
|
|
with what's in Tracker."""
|
|
ret = []
|
|
keys = ss_issues[0].keys()
|
|
for t_issue in t_issues:
|
|
commit = {}
|
|
ss_issue = self.spreadsheet_issue_for_id(ss_issues, t_issue['id'])
|
|
if ss_issue is None:
|
|
# New issue
|
|
commit['new_row'] = self.tracker_issue_to_spreadsheet_issue(t_issue,
|
|
keys)
|
|
commit['type'] = 'append'
|
|
elif self.spreadsheet_row_needs_update(ss_issue, t_issue):
|
|
commit['__raw_entry'] = ss_issue['__raw_entry']
|
|
del ss_issue['__raw_entry']
|
|
ss_issue.update(self.tracker_issue_to_spreadsheet_issue(t_issue, keys))
|
|
commit['dict'] = ss_issue
|
|
commit['type'] = 'update'
|
|
else:
|
|
continue
|
|
ret.append(commit)
|
|
return ret
|
|
|
|
def tracker_to_spreadsheet(self):
|
|
"""High-level function to migrate data from Tracker to the spreadsheet."""
|
|
(t_issues, ss_issues) = self.fetch_issues()
|
|
if len(ss_issues) == 0:
|
|
raise Exception('Error: must have at least one non-header row in '\
|
|
'spreadsheet')
|
|
return
|
|
ss_keys = ss_issues[0].keys()
|
|
|
|
print 'Calculating deltas...'
|
|
ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues)
|
|
self.debug('commits: %s' % ss_commits)
|
|
if not ss_commits:
|
|
print 'Nothing to commit.'
|
|
return
|
|
print 'Committing...'
|
|
for commit in ss_commits:
|
|
self.debug('Operating on commit: %s' % commit)
|
|
if commit['type'] == 'append':
|
|
print 'Appending new row...'
|
|
if not self.pretend:
|
|
self.gd_client.InsertRow(commit['new_row'],
|
|
self.ss_key, self.ss_ws_key)
|
|
else:
|
|
print '(Skipped because --pretend set)'
|
|
if commit['type'] == 'update':
|
|
print 'Updating row...'
|
|
if not self.pretend:
|
|
self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict'])
|
|
else:
|
|
print '(Skipped because --pretend set)'
|
|
print 'Done.'
|
|
|
|
def main():
|
|
class PureEpilogOptionParser(OptionParser):
|
|
def format_epilog(self, formatter):
|
|
return self.epilog
|
|
|
|
parser = PureEpilogOptionParser()
|
|
parser.add_option('-a', '--action', dest='action', metavar='ACTION',
|
|
help='Action to perform')
|
|
parser.add_option('-d', '--debug', action='store_true', dest='debug',
|
|
default=False, help='Print debug output.')
|
|
parser.add_option('-m', '--message', dest='message', metavar='TEXT',
|
|
help='Log message when updating Tracker issues')
|
|
parser.add_option('-p', '--pretend', action='store_true', dest='pretend',
|
|
default=False, help="Don't commit anything.")
|
|
parser.add_option('--ss_key', dest='ss_key', metavar='KEY',
|
|
help='Spreadsheets key (find with browse action)')
|
|
parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY',
|
|
help='Spreadsheets worksheet key (find with browse action)')
|
|
parser.add_option('--tracker_project', dest='tracker_project',
|
|
metavar='PROJECT',
|
|
help='Tracker project (default: chromium-os)',
|
|
default='chromium-os')
|
|
parser.epilog = """Actions:
|
|
browse -- browse spreadsheets to find spreadsheet and worksheet keys.
|
|
ss_to_t -- for each entry in spreadsheet, apply its values to tracker.
|
|
If no ID is in the spreadsheet row, a new tracker item is created
|
|
and the spreadsheet is updated.
|
|
t_to_ss -- for each tracker entry, apply it or add it to the spreadsheet.
|
|
|
|
|
|
This script can be used to migrate Issue Tracker issues between Issue Tracker
|
|
and Google Spreadsheets. The spreadsheet should have certain columns in any
|
|
order: Id, Owner, Title, Status. The spreadsheet may have any label of the
|
|
form 'Key-'. For those labels that end in '-', this script assumes the cell
|
|
value and the header form a label that should be applied to the issue. E.g.
|
|
if the spredsheet has a column named 'Mstone-' and a cell under it called
|
|
'R8' that corresponds to the label 'Mstone-R8' in Issue Tracker.
|
|
|
|
To migrate data, you must choose on each invocation of this script if you
|
|
wish to migrate data from Issue Tracker to a spreadsheet of vice-versa.
|
|
|
|
When migrating from Tracker, all found issues based on the query
|
|
(which is currently hard-coded to "label=Area-Installer") will be inserted
|
|
into the spreadsheet (overwritng existing cells if a row with matching ID
|
|
is found). Custom columns in the spreadsheet won't be overwritten, so if
|
|
the spreadsheet contains extra columns about issues (e.g. time estimates)
|
|
they will be preserved.
|
|
|
|
When migrating from spreadsheet to Tracker, each row in the spreadsheet
|
|
is compared to existing tracker issues that match the query
|
|
(which is currently hard-coded to "label=Area-Installer"). If the
|
|
spreadsheet row has no Id, a new Issue Tracker issue is created and the new
|
|
Id is written back to the spreadsheet. If an existing tracker issue exists,
|
|
it's updated with the data from the spreadsheet if anything has changed.
|
|
|
|
Suggested usage:
|
|
- Create a spreadsheet with columns Id, Owner, Title, Status, and any label
|
|
prefixes as desired.
|
|
- Run this script with '-b' to browse your spreadsheet and get the
|
|
spreadsheet key.
|
|
- Run this script again with '-b' and the spreadsheet key to get the
|
|
worksheet key.
|
|
- Run this script with "-a t_to_ss" or "-a ss_to_t" to migrate data in either
|
|
direction.
|
|
|
|
Known issues:
|
|
- query is currently hardcoded to label=Area-Installer. That should be
|
|
a command-line flag.
|
|
- When creating a new issue on tracker, the owner field isn't set. I (adlr)
|
|
am not sure why. Workaround: If you rerun this script, tho, it will detect
|
|
a delta and update the tracker issue with the owner, which seems to succeed.
|
|
"""
|
|
|
|
(options, args) = parser.parse_args()
|
|
|
|
merger = Merger(options.ss_key, options.ss_ws_key,
|
|
options.message, options.tracker_project,
|
|
options.debug, options.pretend)
|
|
if options.action == 'browse':
|
|
merger.browse()
|
|
elif options.action == 'ss_to_t':
|
|
if not options.message:
|
|
print 'Error: when updating tracker, -m MESSAGE required.'
|
|
return
|
|
merger.spreadsheet_to_tracker()
|
|
elif options.action == 't_to_ss':
|
|
merger.tracker_to_spreadsheet()
|
|
else:
|
|
raise Exception('Unknown action requested.')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|